Adding Social Sign In to a Spring MVC Web Application: Unit Testing

Spring Social 1.0 has a spring-social-test module which provides support for testing Connect implementations and API bindings. This module was removed from Spring Social 1.1.0, and it was replaced with the Spring MVC Test framework.

The problem is that there is practically no information about writing unit tests for an application which uses Spring Social 1.1.0.

This blog post fixes that problem.

During this blog post we will learn how we can write unit tests for the registration function of our example application which we created in the previous parts of this Spring Social tutorial.

If you have not read the previous parts of my Spring Social tutorial, I recommend that you read them before reading this blog post. The prerequisites of this blog post are described in the following:

Let's start by finding out how we can get the required testing decencies with Maven.

Getting the Required Dependencies with Maven

We can get the required testing dependencies by declaring the following dependencies in our POM file:

  • AssertJ (version 1.6.0). AssertJ is a library which provides fluent interface for writing assertions.
  • hamcrest-all (version 1.4). We use Hamcrest matchers for writing assertions in our unit tests.
  • JUnit (version 4.11). We also need to exclude the hamcrest-core because we already added the hamcrest-all dependency.
  • mockito-all (version 1.9.5). We use Mockito as our mocking library.
  • Catch-Exception (version 1.2.0). The catch-exception library helps us to catch exceptions without terminating the execution of our test methods and makes the catched exceptions available for further analysis. We need to exclude the mockito-core dependency because we already added the mockito-all dependency.
  • Spring Test (version 3.2.4.RELEASE). The Spring Test Framework is a framework which makes it possible to write tests for Spring powered applications.

The relevant part of the pom.xml file looks as follows:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>1.6.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.9.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.googlecode.catch-exception</groupId>
    <artifactId>catch-exception</artifactId>
    <version>1.2.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.4.RELEASE</version>
    <scope>test</scope>
</dependency>

Let's move and take a quick look under the hood of Spring Social.

Looking Under the Hood of Spring Social

As we might remember from the second part of this tutorial, the RegistrationController class is responsible of rendering the registration form and processing the form submissions of the registration form. It uses the ProviderSignInUtils class for two purposes:

  1. When the registration form is rendered, the RegistrationController class pre-populates the form fields if the user is creating a new user account by using social sign in. The form object is pre-populated by using the information provided by the used SaaS API provider. This information is stored to a Connection object. The controller class obtains the Connection object by calling the static getConnection() method of the ProviderSignInUtils class.
  2. After a new user account has been created, the RegistrationConnection class persists the Connection object to the database if the user account was created by using social sign in. The controller class does this by calling the handlePostSignUp() method of the ProviderSignInUtils class.

If we want understand the role of the ProviderSignInUtils class, we have take a look at its source code. The source code of the ProviderSignInUtils class looks as follows:

package org.springframework.social.connect.web;

import org.springframework.social.connect.Connection;
import org.springframework.web.context.request.RequestAttributes;

public class ProviderSignInUtils {
	
	public static Connection<?> getConnection(RequestAttributes request) {
		ProviderSignInAttempt signInAttempt = getProviderUserSignInAttempt(request);
		return signInAttempt != null ? signInAttempt.getConnection() : null;
	}

	public static void handlePostSignUp(String userId, RequestAttributes request) {
		ProviderSignInAttempt signInAttempt = getProviderUserSignInAttempt(request);
		if (signInAttempt != null) {
			signInAttempt.addConnection(userId);
			request.removeAttribute(ProviderSignInAttempt.SESSION_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
		}		
	}
	
	private static ProviderSignInAttempt getProviderUserSignInAttempt(RequestAttributes request) {
		return (ProviderSignInAttempt) request.getAttribute(ProviderSignInAttempt.SESSION_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
	}
}

We can see two things from the source code of the ProviderSignInUtils class:

  1. The getConnection() method gets a ProviderSignInAttempt object from session. If the obtained object is null, it returns null. Otherwise it calls the getConnection() method of the ProviderSignInAttempt class and returns the Connection object.
  2. The handlePostSignUp() method gets a ProviderSignInAttempt object from session. If the object is found, it calls the addConnection() method of the ProviderSignInAttempt class and removes the found ProviderSignInAttempt object from the session.

It is clear that in order to write unit tests for the RegistrationController class, we have to figure out a way to create ProviderSignInAttempt objects, and set the created objects to session.

Let's find out how this is done.

Creating Test Doubles

As we figured out, if we want to write unit tests for the RegistrationController class, we have to find a way to create ProviderSignInAttempt objects. This section describes how we can achieve this goal by using test doubles.

Let's move on and find out how we can create ProviderSignInAttempt objects in our unit tests.

Creating ProviderSignInAttempt Objects

If we want to understand how we can create ProviderSignInAttempt objects, we have to take a closer look at its source code. The source code of the ProviderSignInAttempt class looks as follows:

package org.springframework.social.connect.web;

import java.io.Serializable;

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.DuplicateConnectionException;
import org.springframework.social.connect.UsersConnectionRepository;

@SuppressWarnings("serial")
public class ProviderSignInAttempt implements Serializable {

	public static final String SESSION_ATTRIBUTE = ProviderSignInAttempt.class.getName();

	private final ConnectionData connectionData;
	
	private final ConnectionFactoryLocator connectionFactoryLocator;
	
	private final UsersConnectionRepository connectionRepository;
		
	public ProviderSignInAttempt(Connection<?> connection, ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository connectionRepository) {
		this.connectionData = connection.createData();
		this.connectionFactoryLocator = connectionFactoryLocator;
		this.connectionRepository = connectionRepository;		
	}
		
	public Connection<?> getConnection() {
		return connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()).createConnection(connectionData);
	}

	void addConnection(String userId) {
		connectionRepository.createConnectionRepository(userId).addConnection(getConnection());
	}
}

As we can see, the ProviderSignInAttempt class has three dependencies which are described in the following:

The first thing that comes to mind is to mock these dependencies. Although this might seem like a good idea, this approach has two problems:

  1. We would have to configure the behavior of our mock objects in every test we write. This means that our tests would be harder to understand.
  2. We are leaking the implementation details of Spring Social into our tests. This would make our tests harder to maintain because if the implementation of Spring Social changes, our tests could be broken.

It is clear that mocking is not best solution for this problem. We must remember that even though mocking is a valuable and a handy testing tool, we should not overuse it.

This creates a new question:

If mocking is out of the question, what is the right tool for the job?

The answer to this question is found from an article written by Martin Fowler. In this article, Martin Fowler specifies a test double called a stub as follows:

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent’.

Using a stub makes perfect sense because we are interested in two things:

  1. We need to be able to configure the Connection object returned by our stub.
  2. We need to verify that the connection was persisted to the database after a new user account has been created.

We can create a stub which fulfills these goals by following these steps:

  1. Create a TestProviderSignInAttempt class which extends the ProviderSignInAttempt class.
  2. Add a private connection field to the class and set the type of the added field to Connection. This field contains a reference to connection between the user and the SaaS API provider.
  3. Add a private connections field to the class and set the type of the added to field to Set. This field contains the user ids of the persisted connections.
  4. Add a constructor which takes a Connection object as a constructor argument to the created class. Implement the constructor by following these steps:
    1. Call the constructor of the ProviderSignInAttempt class and pass the Connection object as a constructor argument. Set the values of other constructor arguments to null.
    2. Set the Connection object given as a constructor argument to the connection field.
  5. Override the getConnection() method of the ProviderSignInAttempt class and implement it by returning the object stored to the connection field.
  6. Override the addConnection(String userId) method of the ProviderSignInAttempt class and implement it by adding the user id given as a method parameter to the connections set.
  7. Add a public getConnections() method to the created class and implement it by returning the connections set.

The source code of the TestProviderSignInAttempt looks as follows:

package org.springframework.social.connect.web;

import org.springframework.social.connect.Connection;

import java.util.HashSet;
import java.util.Set;

public class TestProviderSignInAttempt extends ProviderSignInAttempt {

    private Connection<?> connection;

    private Set<String> connections = new HashSet<>();

    public TestProviderSignInAttempt(Connection<?> connection) {
        super(connection, null, null);
        this.connection = connection;
    }

    @Override
    public Connection<?> getConnection() {
        return connection;
    }

    @Override
    void addConnection(String userId) {
        connections.add(userId);
    }

    public Set<String> getConnections() {
        return connections;
    }
}

Let's move on and find out how we can create the Connection class which is used in our unit tests.

Creating the Connection Class

The created connection class is a stub class which simulates the behavior of "real" connection classes, but it doesn’t implement any logic associated with OAuth1 and OAuth2 connections. Also, this class must implement the Connection interface.

We can create this stub class by following these steps:

  1. Create a TestConnection class which extends the AbstractConnection class. The AbstractConnection class is a base class which defines the state and behavior shared by all connection implementations.
  2. Add a connectionData field to the created class. Set the type of the field to ConnectionData. The ConnectionData is a data transfer object which contains the internal state of the connection to the used SaaS API provider.
  3. Add a userProfile field to created class. Set the type of the field to UserProfile. This class represents the user profile of the used SaaS API provider and it contains the information which is shared between different service providers.
  4. Create a constructor which takes ConnectionData and UserProfile objects as constructor arguments and implement it by following these steps:
    1. Call the constructor of the AbstractConnection class and pass the ConnectionData object as the first constructor argument. Set the second constructor argument to null.
    2. Set the value of the connectionData field.
    3. Set the value of the userProfile field.
  5. Override the fetchUserProfile() method of the AbstractConnection class, and implement it by returning the object stored to the userProfile field.
  6. Override the getAPI() method of the AbstractConnection class, and implement it by returning null.
  7. Override the createData() method of AbstractConnection class, and implement it by returning the object stored to the connectionData field.

The source code of the TestConnection class looks as follows:

package org.springframework.social.connect.support;

import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.UserProfile;

public class TestConnection extends AbstractConnection {

    private ConnectionData connectionData;

    private UserProfile userProfile;

    public TestConnection(ConnectionData connectionData, UserProfile userProfile) {
        super(connectionData, null);
        this.connectionData = connectionData;
        this.userProfile = userProfile;
    }

    @Override
    public UserProfile fetchUserProfile() {
        return userProfile;
    }

    @Override
    public Object getApi() {
        return null;
    }

    @Override
    public ConnectionData createData() {
        return connectionData;
    }
}

Let's move on and figure out how we can create these test doubles in our unit tests.

Creating the Builder Class

We have now created the stub classes for our unit tests. Our last step is to figure out how we can create TestProviderSignInAttempt objects by using these classes.

At this point, we know that

  1. The constructor of the TestProviderSignInAttempt class takes a Connection object as a constructor argument.
  2. The constructor of the TestConnection class takes ConnectionData and UserProfile objects as constructor arguments.

This means that we can create new TestProviderSignInAttempt objects by following these steps:

  1. Create a new ConnectionData object. The ConnectionData class has a single constructor which takes the required fields as constructor arguments.
  2. Create a new UserProfile object. We can create new UserProfile objects by using the UserProfileBuilder class.
  3. Create a new TestConnection object and pass the created ConnectionData and UserProfile objects as constructor arguments.
  4. Create a new TestProviderSignInAttempt object and pass the created TestConnectionConnection object as a constructor argument.

The source code which creates a new TestProviderSignInAttempt object looks as follows:

 ConnectionData connectionData = new ConnectionData("providerId",
                 "providerUserId",
                 "displayName",
                 "profileUrl",
                 "imageUrl",
                 "accessToken",
                 "secret",
                 "refreshToken",
                 1000L);
 
 UserProfile userProfile = userProfileBuilder
                .setEmail("email")
                .setFirstName("firstName")
                .setLastName("lastName")
                .build();
				
TestConnection connection = new TestConnection(connectionData, userProfile);
TestProviderSignInAttempt signIn = new TestProviderSignInAttempt(connection);

The good news is that we now know how we can create TestProviderSignInAttempt objects in our tests. The bad news is that we cannot use this code in our tests.

We must remember that we aren't writing unit tests just to ensure that our code works as expected. Each test case should also reveal how our code behaves in a specific situation. If we create TestProviderSignInAttempt by adding this code to each test case, we put too much emphasis on creating the objects required by our test cases. This means that the test case is harder to read, and the "essence" of the test case is lost.

Instead, we will create a test data builder class which provides a fluent API for creating TestProviderSignInAttempt objects. We can create this class by following these steps:

  1. Create a class called TestProviderSignInAttemptBuilder.
  2. Add all fields required to create new ConnectionData and UserProfile objects to the TestProviderSignInAttemptBuilder class.
  3. Add methods which are used to set field values of the added fields. Implement each method by following these steps:
    1. Set the value given as method parameter to the correct field.
    2. Return a reference to the TestProviderSignInAttemptBuilder object.
  4. Add connectionData() and userProfile() methods to the TestProviderSignInAttemptBuilder class. These methods simply return a reference to the TestProviderSignInAttemptBuilder object, and their purpose is to make our API more readable.
  5. Add build() method to the test data builder class. This creates the TestProviderSignInAttempt object by following the steps described earlier and returns the created object.

The source code of the TestProviderSignInAttemptBuilder class looks as follows:

package org.springframework.social.connect.support;

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.UserProfile;
import org.springframework.social.connect.UserProfileBuilder;
import org.springframework.social.connect.web.TestProviderSignInAttempt;

public class TestProviderSignInAttemptBuilder {

    private String accessToken;

    private String displayName;

    private String email;

    private Long expireTime;

    private String firstName;

    private String imageUrl;

    private String lastName;

    private String profileUrl;

    private String providerId;

    private String providerUserId;

    private String refreshToken;

    private String secret;

    public TestProviderSignInAttemptBuilder() {

    }

    public TestProviderSignInAttemptBuilder accessToken(String accessToken) {
        this.accessToken = accessToken;
        return this;
    }

    public TestProviderSignInAttemptBuilder connectionData() {
        return this;
    }

    public TestProviderSignInAttemptBuilder displayName(String displayName) {
        this.displayName = displayName;
        return this;
    }

    public TestProviderSignInAttemptBuilder email(String email) {
        this.email = email;
        return this;
    }

    public TestProviderSignInAttemptBuilder expireTime(Long expireTime) {
        this.expireTime = expireTime;
        return this;
    }

    public TestProviderSignInAttemptBuilder firstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public TestProviderSignInAttemptBuilder imageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
        return this;
    }

    public TestProviderSignInAttemptBuilder lastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

    public TestProviderSignInAttemptBuilder profileUrl(String profileUrl) {
        this.profileUrl = profileUrl;
        return this;
    }

    public TestProviderSignInAttemptBuilder providerId(String providerId) {
        this.providerId = providerId;
        return this;
    }

    public TestProviderSignInAttemptBuilder providerUserId(String providerUserId) {
        this.providerUserId = providerUserId;
        return this;
    }

    public TestProviderSignInAttemptBuilder refreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
        return this;
    }

    public TestProviderSignInAttemptBuilder secret(String secret) {
        this.secret = secret;
        return this;
    }

    public TestProviderSignInAttemptBuilder userProfile() {
        return this;
    }

    public TestProviderSignInAttempt build() {
        ConnectionData connectionData = new ConnectionData(providerId,
                providerUserId,
                displayName,
                profileUrl,
                imageUrl,
                accessToken,
                secret,
                refreshToken,
                expireTime);

        UserProfile userProfile = new UserProfileBuilder()
                .setEmail(email)
                .setFirstName(firstName)
                .setLastName(lastName)
                .build();

        Connection connection = new TestConnection(connectionData, userProfile);

        return new TestProviderSignInAttempt(connection);
    }
}
We don’t need to call all methods of this builder class when we are writing unit tests for the RegistrationController class. I added those fields mainly because they will be useful when we write integration tests for our example application.

The code which creates new TestProviderSignInAttempt object is now a lot cleaner and more readable:

TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("email")
                    .firstName("firstName")
                    .lastName("lastName")
                .build();

Let's move on and find out how we can clean up our unit tests by using custom AssertJ.

Creating Custom Assertions

We can clean up our unit tests by replacing the standard JUnit assertions with custom AssertJ assertions. We have to create four custom assertion classes which are described in the following:

  • The first assertion class is used to write assertions for ExampleUserDetails objects. The ExampleUserDetails class contains the information of a logged in user which is stored to the SecurityContext of our application. In other words, the assertions provided by this class are used to verify that the information of the logged in user is correct.
  • The second assertion class is used to write assertions for SecurityContext objects. This class is used write assertions for the user whose information is stored to the SecurityContext.
  • The third assertion class is used to write assertions for TestProviderSignInAttempt objects. This assertion class is used to verify if a connection to a SaaS API provider was created by using the TestProviderSignInAttempt object.
  • The fourth assertion class is used to write assertions for RegistrationForm objects. This class is used to verify that the RegistrationForm object which is passed to our service method contains the correct information.
If you are not familiar with AssertJ, you should read my blog post which explains how you can create custom assertions by using AssertJ, and why you should consider doing this.

Also, you should remember that the implementation of each "assertion method" follows these steps:

  1. We ensure that the actual value is not null.
  2. We create a custom error message which is shown if the assertion fails.
  3. We ensure that actual value is the same than the expected value.
  4. We return a reference to the custom assertion object. This ensures that our custom assertion class provides a fluent API.

These steps aren't repeated in the following subsections if the implementation of the assertion method is trivial.

Let's move on.

Creating the ExampleUserDetailsAssert Class

We can implement the first custom assertion class by following these steps:

  1. Create a ExampleUserDetailsAssert class which extends the AbstractAssert class. Provide the following type parameters:
    1. The first type parameter is the type of the custom assertion. Set the value of this type parameter to ExampleUserDetailsAssert.
    2. The second type parameter is the type of the actual value object. Set the value of this type parameter to ExampleUserDetails.
  2. Add a private constructor to the created class. This constructor takes an ExampleUserDetails object as a constructor argument. Implement the controller by calling the constructor of the superclass and passing the following objects as constructor arguments:
    1. The first constructor argument is the actual value object. Pass the object given as a constructor argument forward to the constructor of the superclass.
    2. The second constructor argument is a Class object which specifies the type of the custom assertion class. Set the value of this constructor argument to ExampleUserDetailsAssert.class.
  3. Add a public static assertThat() method to the created class. This method takes an ExampleUserDetails object as a method parameter. Implement this method by creating a new ExampleUserDetailsAssert object.
  4. Add a public hasFirstName() method to the ExampleUserDetailsAssert class. This method takes the expected first name as a method parameter and returns an ExampleUserDetailsAssert object.
  5. Add a public hasId() method to the ExampleUserDetailsAssert class. This method takes the expected id as a method parameter and return an ExampleUserDetailsAssert object.
  6. Add a public hasLastName() method to the ExampleUserDetailsAssert class. This method takes the expected last name as a method parameter and returns an ExampleUserDetailsAssert object.
  7. Add a public hasPassword() method to the ExampleUserDetailsAssert class. This method takes the expected password as a method parameter and returns an ExampleUserDetailsAssert object.
  8. Add a public hasUsername() method to the ExampleUserDetailsAssert class. This method takes the expected username as a method parameter and returns an ExampleUserDetailsAssert object.
  9. Add a public isActive() method to the ExampleUserDetailsAssert class. This method takes no method parameters and it returns an ExampleUserDetailsAssert object. This method verifies that the user account is not expired or locked. It also ensures that the credentials of the user are not expired and the user account is enabled.
  10. Add a public isRegisteredUser() method to the ExampleUserDetailsAssert class. This method takes no method parameters and it returns an ExampleUserDetailsAssert object. This method verifies that the user has only one role and that role is Role.ROLE_USER.
  11. Add a public isRegisteredByUsingFormRegistration() method to the ExampleUserDetailsAssert class. This method returns an ExampleUserDetailsAssert object.
  12. Add a public isSignedInByUsingSocialSignInProvider() method to the ExampleUserDetailsAssert class. This method takes a SocialMediaService enum (the expected sign in provider) as a method parameter and returns an ExampleUserDetailsAssert object.

The source code of the ExampleUserDetailsAssert class looks as follows:

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class ExampleUserDetailsAssert extends AbstractAssert<ExampleUserDetailsAssert, ExampleUserDetails> {

    private ExampleUserDetailsAssert(ExampleUserDetails actual) {
        super(actual, ExampleUserDetailsAssert.class);
    }

    public static ExampleUserDetailsAssert assertThat(ExampleUserDetails actual) {
        return new ExampleUserDetailsAssert(actual);
    }

    public ExampleUserDetailsAssert hasFirstName(String firstName) {
        isNotNull();

        Assertions.assertThat(actual.getFirstName())
                .overridingErrorMessage("Expected first name to be <%s> but was <%s>",
                        firstName,
                        actual.getFirstName()
                )
                .isEqualTo(firstName);

        return this;
    }

    public ExampleUserDetailsAssert hasId(Long id) {
        isNotNull();

        Assertions.assertThat(actual.getId())
                .overridingErrorMessage( "Expected id to be <%d> but was <%d>",
                        id,
                        actual.getId()
                )
                .isEqualTo(id);

        return this;
    }

    public ExampleUserDetailsAssert hasLastName(String lastName) {
        isNotNull();

        Assertions.assertThat(actual.getLastName())
                .overridingErrorMessage("Expected last name to be <%s> but was <%s>",
                        lastName,
                        actual.getLastName()
                )
                .isEqualTo(lastName);

        return this;
    }

    public ExampleUserDetailsAssert hasPassword(String password) {
        isNotNull();

        Assertions.assertThat(actual.getPassword())
                .overridingErrorMessage("Expected password to be <%s> but was <%s>",
                        password,
                        actual.getPassword()
                )
                .isEqualTo(password);

        return this;
    }

    public ExampleUserDetailsAssert hasUsername(String username) {
        isNotNull();

        Assertions.assertThat(actual.getUsername())
                .overridingErrorMessage("Expected username to be <%s> but was <%s>",
                        username,
                        actual.getUsername()
                )
                .isEqualTo(username);

        return this;
    }

    public ExampleUserDetailsAssert isActive() {
        isNotNull();

        Assertions.assertThat(actual.isAccountNonExpired())
                .overridingErrorMessage("Expected account to be non expired but it was expired")
                .isTrue();

        Assertions.assertThat(actual.isAccountNonLocked())
                .overridingErrorMessage("Expected account to be non locked but it was locked")
                .isTrue();

        Assertions.assertThat(actual.isCredentialsNonExpired())
                .overridingErrorMessage("Expected credentials to be non expired but they were expired")
                .isTrue();

        Assertions.assertThat(actual.isEnabled())
                .overridingErrorMessage("Expected account to be enabled but it was not")
                .isTrue();

        return this;
    }

    public ExampleUserDetailsAssert isRegisteredUser() {
        isNotNull();

        Assertions.assertThat(actual.getRole())
                .overridingErrorMessage( "Expected role to be <ROLE_USER> but was <%s>",
                        actual.getRole()
                )
                .isEqualTo(Role.ROLE_USER);

        Collection<? extends GrantedAuthority> authorities = actual.getAuthorities();

        Assertions.assertThat(authorities.size())
                .overridingErrorMessage( "Expected <1> granted authority but found <%d>",
                        authorities.size()
                )
                .isEqualTo(1);

        GrantedAuthority authority = authorities.iterator().next();

        Assertions.assertThat(authority.getAuthority())
                .overridingErrorMessage( "Expected authority to be <ROLE_USER> but was <%s>",
                        authority.getAuthority()
                )
                .isEqualTo(Role.ROLE_USER.name());

        return this;
    }

    public ExampleUserDetailsAssert isRegisteredByUsingFormRegistration() {
        isNotNull();

        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage( "Expected socialSignInProvider to be <null> but was <%s>",
                        actual.getSocialSignInProvider()
                )
                .isNull();

        return this;
    }

    public ExampleUserDetailsAssert isSignedInByUsingSocialSignInProvider(SocialMediaService socialSignInProvider) {
        isNotNull();

        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage( "Expected socialSignInProvider to be <%s> but was <%s>",
                        socialSignInProvider,
                        actual.getSocialSignInProvider()
                )
                .isEqualTo(socialSignInProvider);

        return this;
    }
}

Creating the SecurityContextAssert Class

We can create the second customer assertion class by following these steps:

  1. Create a SecurityContextAssert class which extends the AbstractAssert class. Provide the following type parameters:
    1. The first type parameter is the type of the custom assertion. Set the value of this type parameter to SecurityContextAssert.
    2. The second type parameter is the type of the actual value object. Set the value of this type parameter to SecurityContext.
  2. Add a private constructor to the created class. This constructor takes a SecurityContext object as a constructor argument. Implement the controller by calling the constructor of the superclass and passing the following objects as constructor arguments:
    1. The first constructor argument is the actual value object. Pass the object given as a constructor argument forward to the constructor of the superclass.
    2. The second constructor argument is a Class object which specifies the type of the custom assertion class. Set the value of this constructor argument to SecurityContextAssert.class.
  3. Add a public static assertThat() method to the created class. This method takes a SecurityContext object as a method parameter. Implement this method by creating a new SecurityContextAssert object.
  4. Add a public userIsAnonymous() method to the SecurityContextAssert class, and implement it by following these steps:
    1. Ensure that the actual SecurityContext objects is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Get the Authentication object from the SecurityContext and ensure that it is null.
    3. Return a reference to the SecurityContextAssert object.
  5. Add a public loggedInUserIs() method to the SecurityContextAssert class. This method takes a User object (the expected logged in user) as a method parameter and returns a SecurityContextAssert object. We can implement this method by following these steps:
    1. Ensure that the actual SecurityContext object is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Get the ExampleUserDetails object from the SecurityContext and ensure that it is not null.
    3. Ensure that the information of the ExampleUserDetails object is equal to the information of the User object.
    4. Return a reference to the SecurityContextAssert object.
  6. Add a public loggedInUserHasPassword() method to the SecurityContextAssert class. This method takes the expected password as a method parameter and returns a SecurityContextAssert object. We can implement this method by following these steps:
    1. Ensure that the actual SecurityContext object is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Get the ExampleUserDetails object from the SecurityContext and ensure that it is not null.
    3. Ensure that the ExampleUserDetails object's password field is equal to the password given as a method parameter.
    4. Return a reference to the SecurityContextAssert object.
  7. Add a public loggedInUserIsRegisteredByUsingNormalRegistration() method to the SecurityContextAssert class and implement it by following these steps:
    1. Ensure that the actual SecurityContext object is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Get the ExampleUserDetails object from the SecurityContext and ensure that it is not null.
    3. Ensure that the user account is created by using normal registration.
    4. Return a reference to the SecurityContextAssert object.
  8. Add a public loggedInUserIsSignedInByUsingSocialProvider() method to the SecurityContextAssert class. This method takes a SocialMediaService enum (the expected social sign in provider) as a method parameter and returns a SecurityContextAssert object. We can implement this method by following these steps:
    1. Ensure that the actual SecurityContext object is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Get the ExampleUserDetails object from the SecurityContext and ensure that it is not null.
    3. Ensure that the user account is created by using the SociaMediaService given as a method parameter.
    4. Return a reference to the SecurityContextAssert object.

The source code of the SecurityContextAssert class looks as follows:

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;

public class SecurityContextAssert extends AbstractAssert<SecurityContextAssert, SecurityContext> {

    private SecurityContextAssert(SecurityContext actual) {
        super(actual, SecurityContextAssert.class);
    }

    public static SecurityContextAssert assertThat(SecurityContext actual) {
        return new SecurityContextAssert(actual);
    }

    public SecurityContextAssert userIsAnonymous() {
        isNotNull();

        Authentication authentication = actual.getAuthentication();

        Assertions.assertThat(authentication)
                .overridingErrorMessage("Expected authentication to be <null> but was <%s>.",
                        authentication
                )
                .isNull();

        return this;
    }

    public SecurityContextAssert loggedInUserIs(User user) {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <%s> but was <null>",
                        user
                )
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasFirstName(user.getFirstName())
                .hasId(user.getId())
                .hasLastName(user.getLastName())
                .hasUsername(user.getEmail())
                .isActive()
                .isRegisteredUser();

        return this;
    }

    public SecurityContextAssert loggedInUserHasPassword(String password) {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <not null> but was <null>")
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasPassword(password);

        return this;
    }

    public SecurityContextAssert loggedInUserIsRegisteredByUsingNormalRegistration() {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <not null> but was <null>")
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .isRegisteredByUsingFormRegistration();

        return this;
    }

    public SecurityContextAssert loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService signInProvider) {
        isNotNull();

        ExampleUserDetails loggedIn = (ExampleUserDetails) actual.getAuthentication().getPrincipal();

        Assertions.assertThat(loggedIn)
                .overridingErrorMessage("Expected logged in user to be <not null> but was <null>")
                .isNotNull();

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .hasPassword("SocialUser")
                .isSignedInByUsingSocialSignInProvider(signInProvider);

        return this;
    }
}

Creating the TestProviderSignInAttemptAssert Class

We can create the third custom assertion class by following these steps:

  1. Create a TestProviderSignInAttemptAssert class which extends the AbstractAssert class. Provide the following type parameters:
    1. The first type parameter is the type of the custom assertion. Set the value of this type parameter to TestProviderSignInAttemptAssert.
    2. The second type parameter is the type of the actual value object. Set the value of this type parameter to TestProviderSignInAttempt.
  2. Add a private constructor to the created class. This constructor takes a TestProviderSignInAttempt object as a constructor argument. Implement the controller by calling the constructor of the superclass and passing the following objects as constructor arguments:
    1. The first constructor argument is the actual value object. Pass the object given as a constructor argument forward to the constructor of the superclass.
    2. The second constructor argument is a Class object which specifies the type of the custom assertion class. Set the value of this constructor argument to TestProviderSignInAttemptAssert.class.
  3. Add a public static assertThatSignIn() method to the created class. This method takes a TestProviderSignInAttempt object as a method parameter. Implement this method by creating a new TestProviderSignInAttemptAssert object.
  4. Add a public createdNoConnections() method to the created class. This method takes no method parameters and it returns a reference to TestProviderSignInAttemptAssert object. We can implement this method by following these steps:
    1. Ensure that the actual TestProviderSignInAttempt object is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Ensure that the actual TestProviderSignInAttempt object created no connections.
    3. Return a reference to the TestProviderSignInAttemptAssert object.
  5. Add a public createdConnectionForUserId() method to the created class. This method takes a the expected user id as a method parameter and returns a reference to TestProviderSignInAttempt object. We can implement this method by following these steps:
    1. Ensure that the actual TestProviderSignInAttempt object is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Ensure that a connection was created for the user whose user id was given as a method parameter.
    3. Return a reference to the TestProviderSignInAttemptAssert object.

The source code of the TestProviderSignInAttemptAssert class looks as follows:

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.springframework.social.connect.web.TestProviderSignInAttempt;

public class TestProviderSignInAttemptAssert extends AbstractAssert<TestProviderSignInAttemptAssert, TestProviderSignInAttempt> {

    private TestProviderSignInAttemptAssert(TestProviderSignInAttempt actual) {
        super(actual, TestProviderSignInAttemptAssert.class);
    }

    public static TestProviderSignInAttemptAssert assertThatSignIn(TestProviderSignInAttempt actual) {
        return new TestProviderSignInAttemptAssert(actual);
    }

    public TestProviderSignInAttemptAssert createdNoConnections() {
        isNotNull();

        Assertions.assertThat(actual.getConnections())
                .overridingErrorMessage( "Expected that no connections were created but found <%d> connection",
                        actual.getConnections().size()
                )
                .isEmpty();

        return this;
    }

    public TestProviderSignInAttemptAssert createdConnectionForUserId(String userId) {
        isNotNull();

        Assertions.assertThat(actual.getConnections())
                .overridingErrorMessage("Expected that connection was created for user id <%s> but found none.",
                        userId
                )
                .contains(userId);

        return this;
    }
}

Creating the RegistrationFormAssert Class

We can create the fourth custom assertion class by following these steps:

  1. Create a RegistrationFormAssert class which extends the AbstractAssert class. Provide the following type parameters:
    1. The first type parameter is the type of the custom assertion. Set the value of this type parameter to RegistrationFormAssert.
    2. The second type parameter is the type of the actual value object. Set the value of this type parameter to RegistrationForm.
  2. Add a private constructor to the created class. This constructor takes a RegistrationForm object as a constructor argument. Implement the controller by calling the constructor of the superclass and passing the following objects as constructor arguments:
    1. The first constructor argument is the actual value object. Pass the object given as a constructor argument forward to the constructor of the superclass.
    2. The second constructor argument is a Class object which specifies the type of the custom assertion class. Set the value of this constructor argument to RegistrationFormAssert.class.
  3. Add a public static assertThatRegistrationForm() method to the created class. This method takes an RegistrationForm object as a method parameter. Implement this method by returning a new RegistrationFormAssert object.
  4. Add a public hasEmail() method to the created class. This method takes the expected email as a method parameter and returns a RegistrationFormAssert object.
  5. Add a public hasFirstName() method to the created class. This method takes the expected first name as a method parameter and returns a RegistrationFormAssert object.
  6. Add a public hasLastName() method to the created class. This method takes the expected last name as a method parameter and returns a RegistrationFormAssert object.
  7. Add a public hasNoPassword() method to the created class. This method has no method parameters and it returns a RegistrationFormAssert object.
  8. Add a public hasNoPasswordVerification() method to the created class. This method takes no method parameters and it returns a RegistrationFormAssert object.
  9. Add a public hasPassword() method to the created class. This method takes the expected password as a method parameter and returns a RegistrationFormAssert object.
  10. Add a public hasPasswordVerification method to the created class. This method takes the expected password verification as a method parameter and returns a RegistrationFormAssert object.
  11. Add a public isNormalRegistration() method to the created class. This method takes no method parameters and returns a RegistrationFormAssert object.
  12. Add a public isSocialSignInWithSignInProvider() method to the created class. This method takes a SocialMediaService enum as a method parameter (the expected sign in provider) and returns a RegistrationFormAssert object.

The source code of the RegistrationFormAssert class looks as follows:

import org.assertj.core.api.AbstractAssert;

import static org.assertj.core.api.Assertions.assertThat;

public class RegistrationFormAssert extends AbstractAssert<RegistrationFormAssert, RegistrationForm> {

    private RegistrationFormAssert(RegistrationForm actual) {
        super(actual, RegistrationFormAssert.class);
    }

    public static RegistrationFormAssert assertThatRegistrationForm(RegistrationForm actual) {
        return new RegistrationFormAssert(actual);
    }

    public RegistrationFormAssert hasEmail(String email) {
        isNotNull();

        assertThat(actual.getEmail())
                .overridingErrorMessage("Expected email to be <%s> but was <%s>",
                        email,
                        actual.getEmail()
                )
                .isEqualTo(email);

        return this;
    }

    public RegistrationFormAssert hasFirstName(String firstName) {
        isNotNull();

        assertThat(actual.getFirstName())
                .overridingErrorMessage("Expected first name to be <%s> but was <%s>",
                        firstName,
                        actual.getFirstName()
                )
                .isEqualTo(firstName);

        return this;
    }

    public RegistrationFormAssert hasLastName(String lastName) {
        isNotNull();

        assertThat(actual.getLastName())
                .overridingErrorMessage("Expected last name to be <%s> but was <%s>",
                        lastName,
                        actual.getLastName())
                .isEqualTo(lastName);

        return this;
    }

    public RegistrationFormAssert hasNoPassword() {
        isNotNull();

        assertThat(actual.getPassword())
                .overridingErrorMessage("Expected password to be <null> but was <%s>",
                        actual.getPassword()
                )
                .isNull();

        return this;
    }

    public RegistrationFormAssert hasNoPasswordVerification() {
        isNotNull();

        assertThat(actual.getPasswordVerification())
                .overridingErrorMessage("Expected password verification to be <null> but was <%s>",
                        actual.getPasswordVerification()
                )
                .isNull();

        return this;
    }

    public RegistrationFormAssert hasPassword(String password) {
        isNotNull();

        assertThat(actual.getPassword())
                .overridingErrorMessage("Expected password to be <%s> but was <%s>",
                        password,
                        actual.getPassword()
                )
                .isEqualTo(password);

        return this;
    }

    public RegistrationFormAssert hasPasswordVerification(String passwordVerification) {
        isNotNull();

        assertThat(actual.getPasswordVerification())
                .overridingErrorMessage("Expected password verification to be <%s> but was <%s>",
                        passwordVerification,
                        actual.getPasswordVerification()
                )
                .isEqualTo(passwordVerification);

        return this;
    }

    public RegistrationFormAssert isNormalRegistration() {
        isNotNull();

        assertThat(actual.getSignInProvider())
                .overridingErrorMessage("Expected sign in provider to be <null> but was <%s>",
                        actual.getSignInProvider()
                )
                .isNull();

        return this;
    }

    public RegistrationFormAssert isSocialSignInWithSignInProvider(SocialMediaService signInProvider) {
        isNotNull();

        assertThat(actual.getSignInProvider())
                .overridingErrorMessage("Expected sign in provider to be <%s> but was <%s>",
                        signInProvider,
                        actual.getSignInProvider()
                )
                .isEqualTo(signInProvider);

        return this;
    }
}

Let's move on and start writing some unit test for the RegistrationController class.

Writing Unit Tests

We have now finished our preparations and are ready to write unit tests for the registration function. We have to write unit tests for the following controller methods:

  • The first controller method renders the registration page.
  • The second controller method processes the submissions of the registration form.

Before we can start writing our unit tests, we have to configure them. Let's find out how this is done.

Our unit tests use the Spring MVC Test framework. If you are not familiar with it, I recommend that you take a look at my Spring MVC Test tutorial.

Configuring Our Unit Tests

The application context configuration of our example application is designed in a such way that it is easy to write unit tests for the web layer. These design principles are described in the following:

  • The application context configuration is divided into several configuration classes and each class has configures a specific part of our application (web, security, social, and persistence).
  • Our application context configuration has a "main" configuration class which configures a few "general" beans and imports the other configuration classes. This configuration class also configures the component scan for the service layer.

When we configure the application context by following these principles, it is easy to create the application context configuration for our unit tests. We can do this by reusing the application context configuration class which configures the web layer of our example application and creating a new application context configuration class for our unit tests.

We can create the application context configuration class for our unit tests by following these steps:

  1. Create a class called UnitTestContext.
  2. Annotate the created class with the @Configuration annotation.
  3. Add a messageSource() method to created class and annotate the method with the @Bean annotation. Configure the MessageSource bean by following these steps:
    1. Create a new ResourceBundleMessageSource object.
    2. Set the base name of the message files and ensure that if a message is not found, its code is returned.
    3. Return the created object.
  4. Add a userService() method to the created class and annotate the method with the @Bean annotation. Configure the UserService mock object by following these steps:
    1. Call the static mock() method of the Mockito class, and pass UserService.class as a method parameter.
    2. Return the created object.

The source code of the UnitTestContext class looks as follows:

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;

import static org.mockito.Mockito.mock;

@Configuration
public class UnitTestContext {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();

        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);

        return messageSource;
    }

    @Bean
    public UserService userService() {
        return mock(UserService.class);
    }
}

The next thing that we have to do is to configure our unit tests. We can do this by following these steps:

  1. Annotate the test class with the @RunWith annotation and ensure that our tests are executed by using the SpringUnit4ClassRunner.
  2. Annotate the class with the @ContextConfiguration annotation, and ensure that the correct configuration classes are used. In our case, the correct configuration classes are: WebAppContext and UnitTestContext.
  3. Annotate the class with the @WebAppConfiguration annotation. This annotation ensures that the loaded application context is a WebApplicationContext.
  4. Add a MockMvc field to the test class.
  5. Add a WebApplicationContext field to the class and annotate it with the @Autowired annotation.
  6. Add a UserService field to the test class and annotate it with the @Autowired annotation.
  7. Add a setUp() method to the test class and annotate the method with the @Before annotation. This ensures that the method is called before each test method. Implement this method by following these steps:
    1. Reset the UserService mock by calling the static reset() method of the Mockito class and passing the reseted mock as a method parameter.
    2. Create a new MockMvc object by using the MockMvcBuilders class.
    3. Ensure that no Authentication object is found from the SecurityContext when our tests are run. We can do this by following these steps:
      1. Obtain a reference to the SecurityContext object by calling the static getContext() method of the SecurityContextHolder class.
      2. Clear the authentication by calling the setAuthentication() method of the SecurityContext class. Pass null as a method parameter.

The source code of our unit test class looks as follows:

import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest2 {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    @Before
    public void setUp() {
        Mockito.reset(userServiceMock);

        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
                .build();
				
		SecurityContextHolder.getContext().setAuthentication(null);
    }
}
If you want to get more information about the configuration of unit tests which use the Spring MVC Test framework, I recommend that you read this blog post.

Let's move on and write unit tests for a controller method which renders the registration form.

Rendering the Registration Form

The controller method which renders the registration form has one important responsibility:

If the user is using social sign in, the fields of the registration are pre-populated by using the information which is used provided by the used SaaS API provider.

Let's refresh our memory and take a look at the source code of the RegistrationController class:

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionKey;
import org.springframework.social.connect.UserProfile;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.WebRequest;

@Controller
@SessionAttributes("user")
public class RegistrationController {

    @RequestMapping(value = "/user/register", method = RequestMethod.GET)
    public String showRegistrationForm(WebRequest request, Model model) {
        Connection<?> connection = ProviderSignInUtils.getConnection(request);

        RegistrationForm registration = createRegistrationDTO(connection);
        model.addAttribute("user", registration);

        return "user/registrationForm";
    }

    private RegistrationForm createRegistrationDTO(Connection<?> connection) {
        RegistrationForm dto = new RegistrationForm();

        if (connection != null) {
            UserProfile socialMediaProfile = connection.fetchUserProfile();
            dto.setEmail(socialMediaProfile.getEmail());
            dto.setFirstName(socialMediaProfile.getFirstName());
            dto.setLastName(socialMediaProfile.getLastName());

            ConnectionKey providerKey = connection.getKey();
            dto.setSignInProvider(SocialMediaService.valueOf(providerKey.getProviderId().toUpperCase()));
        }

        return dto;
    }
}

It is clear that we have to write two unit tests for this controller method:

  1. We have to write a test which ensures that the controller method is working properly when the user is using "normal" registration.
  2. We have to write a test which ensures that the controller method is working properly when the user is using social sign in.

Let's move and write these unit tests.

Test 1: Rendering a Normal Registration Form

We can write the first unit test by following these steps:

  1. Execute a GET request to url '/user/register'.
  2. Ensure that the HTTP status code 200 is returned.
  3. Verify that the name of the rendered view is 'user/registrationForm'.
  4. Verify that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  5. Ensure that all fields of the model attribute called 'user' are either null or empty.
  6. Verify that no methods of the UserService mock were called.

The source code of our unit test looks as follows:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity

    @Test
    public void showRegistrationForm_NormalRegistration_ShouldRenderRegistrationPageWithEmptyForm() throws Exception {
        mockMvc.perform(get("/user/register"))
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", isEmptyOrNullString()),
                        hasProperty("firstName", isEmptyOrNullString()),
                        hasProperty("lastName", isEmptyOrNullString()),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", isEmptyOrNullString())
                )));

        verifyZeroInteractions(userServiceMock);
    }
}

Test 2: Rendering the Registration Form by Using Social Sign In

We can write the second unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Execute a GET request to url '/user/register' and set the created TestProviderSignInAttempt object to the HTTP session.
  3. Ensure that the HTTP status code 200 is returned.
  4. Verify that the name of the rendered view is 'user/registrationForm'.
  5. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  6. Verify that the fields of the model object called 'user' are pre-populated by using the information contained by the TestProviderSignInAttempt object. We can do this by following these steps:
    1. Ensure that the value of the email field is 'john.smith@gmail.com'.
    2. Ensure that the value of the firstName field is 'John'.
    3. Ensure that the value of the lastName field is 'Smith'.
    4. Ensure that the value of the password field is empty or null String.
    5. Ensure that the value of the passwordVerification field is empty or null String.
    6. Ensure that the value of the signInProvider field is 'twitter'.
  7. Verify that the methods of the UserService interface were not called.

The source code of our unit test looks as follows:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity

    @Test
    public void showRegistrationForm_SocialSignInWithAllValues_ShouldRenderRegistrationPageWithAllValuesSet() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("john.smith@gmail.com")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        mockMvc.perform(get("/user/register")
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", is("john.smith@gmail.com")),
                        hasProperty("firstName", is("John")),
                        hasProperty("lastName", is("Smith")),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is("twitter"))
                )));

        verifyZeroInteractions(userServiceMock);
    }
}

Submitting The Registration Form

The controller method which processes the submissions of the registration form has the following responsibilities:

  1. It validates the information entered to the registration form. If the information is not valid, it renders the registration form and shows validation error messages to user.
  2. If the email address given by the user is not unique, it renders the registration form and shows an error message to the user.
  3. It creates a new user account by using the UserService interface and logs the created user in.
  4. It persists the connection to a SaaS API provider if user was using social sign in
  5. It redirects user to the front page.

The relevant part of the RegistrationController class looks as follows:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.WebRequest;

import javax.validation.Valid;

@Controller
@SessionAttributes("user")
public class RegistrationController {

    private UserService service;

    @Autowired
    public RegistrationController(UserService service) {
        this.service = service;
    }

    @RequestMapping(value ="/user/register", method = RequestMethod.POST)
    public String registerUserAccount(@Valid @ModelAttribute("user") RegistrationForm userAccountData,
                                      BindingResult result,
                                      WebRequest request) throws DuplicateEmailException {
        if (result.hasErrors()) {
            return "user/registrationForm";
        }

        User registered = createUserAccount(userAccountData, result);

        if (registered == null) {
            return "user/registrationForm";
        }
        SecurityUtil.logInUser(registered);
        ProviderSignInUtils.handlePostSignUp(registered.getEmail(), request);

        return "redirect:/";
    }

    private User createUserAccount(RegistrationForm userAccountData, BindingResult result) {
        User registered = null;

        try {
            registered = service.registerNewUserAccount(userAccountData);
        }
        catch (DuplicateEmailException ex) {
            addFieldError(
                    "user",
                    "email",
                    userAccountData.getEmail(),
                    "NotExist.user.email",
                    result);
        }

        return registered;
    }

    private void addFieldError(String objectName, String fieldName, String fieldValue,  String errorCode, BindingResult result) {
        FieldError error = new FieldError(
                objectName,
                fieldName,
                fieldValue,
                false,
                new String[]{errorCode},
                new Object[]{},
                errorCode
        );

        result.addError(error);
    }
}

We will write three unit tests for this controller method:

  1. We write a unit test which ensures that the controller method is working properly when validation fails.
  2. We write a unit test which ensures the the controller method is working when the email address isn't unique.
  3. We write a unit test which ensures that the controller method is working properly when the registration is successful.

Let's find out how we can write these unit tests.

Test 1: Validation Fails

We can write the first unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Execute a POST request to url '/user/register' by following these steps:
    1. Set the content type of the request to 'application/x-www-form-urlencoded'.
    2. Send the value of the signInProvider form field as a request parameter.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set a new RegistrationForm object to the HTTP session. This is required because our controller class is annotated with the @SessionAttributes annotation.
  3. Verify that the HTTP status code 200 is returned.
  4. Ensure that the name of the rendered view is 'user/registrationForm'.
  5. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  6. Verify that field values of the model object called 'user' are correct by following these steps:
    1. Verify that the value of the email field is empty or null String.
    2. Verify that the value of the firstName field is empty or null String.
    3. Verify that the value of the lastName field is empty or null String.
    4. Verify that the value of the password field is empty or null String.
    5. Verify that the value of the passwordVerification field is empty or null String.
    6. Verify that the value of the signInProvider field is SocialMediaService.TWITTER.
  7. Ensure that the model attribute called 'user' has field errors in email, firstName, and lastName fields.
  8. Verify that the current user is not logged in.
  9. Ensure that no connections were created by using the TestProviderSignInAttempt object.
  10. Verify that the methods of the UserService mock were not called.

The source code of our unit test looks as follows:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity

    @Test
    public void registerUserAccount_SocialSignInAndEmptyForm_ShouldRenderRegistrationFormWithValidationErrors() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("john.smith@gmail.com")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("signInProvider", SocialMediaService.TWITTER.name())
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", new RegistrationForm())
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", isEmptyOrNullString()),
                        hasProperty("firstName", isEmptyOrNullString()),
                        hasProperty("lastName", isEmptyOrNullString()),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is(SocialMediaService.TWITTER))
                )))
                .andExpect(model().attributeHasFieldErrors("user", 
						"email", 
						"firstName", 
						"lastName"
				));

        assertThat(SecurityContextHolder.getContext()).userIsAnonymous();
        assertThatSignIn(socialSignIn).createdNoConnections();
        verifyZeroInteractions(userServiceMock);
    }
}

Test 2: Email Address Is Found From the Database

We can write the second unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Configure the UserService mock to throw a DuplicateEmailException when its registerNewUserAccount() method is called and a RegistrationForm object is given as a method parameter.
  3. Execute a POST request to url '/user/register' by following these steps:
    1. Set the content type of the request to 'application/x-www-form-urlencoded'.
    2. Send the values of the email, firstName, lastName, and signInProvider form fields as request parameters.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set a new RegistrationForm object to the HTTP session. This is required because our controller class is annotated with the @SessionAttributes annotation.
  4. Verify that the HTTP status code 200 is returned.
  5. Ensure that the name of the rendered view is 'user/registrationForm'.
  6. Ensure that the request is forwarded to url '/WEB-INF/jsp/user/registrationForm.jsp'.
  7. Verify that field values of the model object called 'user' are correct by following these steps:
    1. Ensure that the value of the email field is 'john.smith@gmail.com'.
    2. Ensure that the value of the firstName field is 'John'.
    3. Ensure that the value of the lastName field is 'Smith'.
    4. Ensure that the value of the password field is empty or null String.
    5. Ensure that the value of the passwordVerification field is empty or null String.
    6. Ensure that the value of the signInProvider field is SocialMediaService.TWITTER.
  8. Ensure that the model attribute called 'user' has field error in email field.
  9. Verify that the current user is not logged in.
  10. Ensure that no connections were created by using the TestProviderSignInAttempt object.
  11. Verify that the registerNewUserAccount() method of the UserService mock was called once and that a RegistrationForm object was given as a method parameter. Capture the method argument by using an ArgumentCaptor.
  12. Verify that the other methods of the UserService interface weren’t invoked during the test.
  13. Verify that the information of the RegistrationForm object, which was passed to our service method, is correct.

The source code of our unit test looks as follows:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity.

    @Test
    public void registerUserAccount_SocialSignInAndEmailExist_ShouldRenderRegistrationFormWithFieldError() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("john.smith@gmail.com")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        when(userServiceMock.registerNewUserAccount(isA(RegistrationForm.class))).thenThrow(new DuplicateEmailException(""));

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
 			   	.param("email", "john.smith@gmail.com")
                .param("firstName", "John")
                .param("lastName", "Smith")
                .param("signInProvider", SocialMediaService.TWITTER.name())
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", new RegistrationForm())
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", is("john.smith@gmail.com")),
                        hasProperty("firstName", is("John")),
                        hasProperty("lastName", is("Smith")),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is(SocialMediaService.TWITTER))
                )))
                .andExpect(model().attributeHasFieldErrors("user", "email"));

        assertThat(SecurityContextHolder.getContext()).userIsAnonymous();
        assertThatSignIn(socialSignIn).createdNoConnections();

		ArgumentCaptor<RegistrationForm> registrationFormArgument = ArgumentCaptor.forClass(RegistrationForm.class);
		verify(userServiceMock, times(1)).registerNewUserAccount(registrationFormArgument.capture());
		verifyNoMoreInteractions(userServiceMock);

		RegistrationForm formObject = registrationFormArgument.getValue();
		assertThatRegistrationForm(formObject)
				.isSocialSignInWithSignInProvider(SocialMediaService.TWITTER)
				.hasEmail("john.smith@gmail.com")
				.hasFirstName("John")
				.hasLastName("Smith")
				.hasNoPassword()
				.hasNoPasswordVerification();
    }
}

Test 3: Registration Is Successful

We can write the third unit test by following these steps:

  1. Create a new TestProviderSignInAttempt object by using the TestProviderSignInAttemptBuilder class. Set the provider id, first name, last name and email address.
  2. Create a new User object by using the UserBuilder class. Set the values of the id, email, firstName, lastName, and signInProvider fields.
  3. Configure the UserService mock object to return the created User object when its registerNewUserAccount() method is called and a RegistrationForm object is given as a method parameter.
  4. Execute a POST request to url '/user/register' by following these steps:
    1. Set the content type of the request to 'application/x-www-form-urlencoded'.
    2. Send the values of the email, firstName, lastName, and signInProvider form fields as request parameters.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set a new RegistrationForm object to the HTTP session. This is required because our controller class is annotated with the @SessionAttributes annotation.
  5. Verify that the HTTP status code 302 is returned.
  6. Ensure that the request is redirected to url '/'.
  7. Verify that the created user is logged in by using Twitter.
  8. Verify that the TestProviderSignInAttempt object was used to created a connection for a user with email address 'john.smith@gmail.com'.
  9. Verify that the registerNewUserAccount() method of the UserService mock was called once and that a RegistrationForm object was given as a method parameter. Capture the method argument by using an ArgumentCaptor.
  10. Verify that the other methods of the UserService interface weren’t invoked during the test.
  11. Verify that the information of the RegistrationForm object, which was passed to our service method, is correct.

The source code of our unit test looks as follows:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebAppContext.class, UnitTestContext.class})
@WebAppConfiguration
public class RegistrationControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Autowired
    private UserService userServiceMock;

    //The setUp() method is omitted for the sake of clarity.

    @Test
    public void registerUserAccount_SocialSignIn_ShouldCreateNewUserAccountAndRenderHomePage() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("john.smith@gmail.com")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        User registered = new UserBuilder()
                .id(1L)
                .email("john.smith@gmail.com")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();

        when(userServiceMock.registerNewUserAccount(isA(RegistrationForm))).thenReturn(registered);

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
 			   	.param("email", "john.smith@gmail.com")
                .param("firstName", "John")
                .param("lastName", "Smith")
                .param("signInProvider", SocialMediaService.TWITTER.name())
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", new RegistrationForm())
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(redirectedUrl("/"));

        assertThat(SecurityContextHolder.getContext())
                .loggedInUserIs(registered)
                .loggedInUserIsSignedInByUsingSocialProvider(SocialMediaService.TWITTER);
        assertThatSignIn(socialSignIn).createdConnectionForUserId("john.smith@gmail.com");

		ArgumentCaptor<RegistrationForm> registrationFormArgument = ArgumentCaptor.forClass(RegistrationForm.class);
		verify(userServiceMock, times(1)).registerNewUserAccount(registrationFormArgument.capture());
		verifyNoMoreInteractions(userServiceMock);

		RegistrationForm formObject = registrationFormArgument.getValue();
		assertThatRegistrationForm(formObject)
				.isSocialSignInWithSignInProvider(SocialMediaService.TWITTER)
				.hasEmail("john.smith@gmail.com")
				.hasFirstName("John")
				.hasLastName("Smith")
				.hasNoPassword()
				.hasNoPasswordVerification();
    }
}

Summary

We have now written some unit tests for the registration function of our example application. This blog post has taught us four things:

  1. We learned how we can create the test doubles required by our unit tests.
  2. We learned to emulate social sign in by using the created test double classes.
  3. We learned how we can verify that the connection to the used SaaS API provider is persisted after a new user account has been created for a user who used social sign in.
  4. We learned how we can verify that the user is logged in after a new user account has been created.

The example application of this blog post has many tests which were not covered in this blog post. If you are interested to see them, you can get the example application from Github.

P.S. This blog post describes one possible approach for writing unit tests to a registration controller which uses Spring Social 1.1.0. If you have any improvement ideas, questions, or feedback about my approach, feel free to leave a comment to this blog post.

If you want to learn how to use Spring Social, you should read my Spring Social tutorial.
9 comments… add one
  • Willian May 12, 2014 @ 17:39

    Hi Petri,

    Congrats, very good tutorial about tests.

    My question is: Whats the needed just to call a persistence Spring Data service or repository, I just need make a test unit like save or merge a Customer JPA Entity for sample, how the config properties are loaded, just using mvn test -P dev is enough ? Can you explain in steps which the order of instancied classes ?

    Regards

    • Petri May 12, 2014 @ 21:40

      Hi Willian,

      if you need to write tests for a Spring Data JPA repository, you should read the following blog posts:

      These blog posts give you all the information you need when you write tests for your Spring Data JPA repositories.

      You can also use the approach described in these blog posts for writing integration tests to service classes.

  • Cassio May 13, 2014 @ 4:44

    Hi Petri,

    This method have to persist the record in database or it's rollbacked?

    I'm trying to persist the Junit test, but when I run mvn test -P dev nothing error messages appear just dont persist. Do you have a idea what this is happening?

    @Test
    public void registerNewUserAccount_ViaSocialSignIn_ShouldCreateNewUserAccountAndSetSocialProvider() throws DuplicateEmailException {
    RegistrationForm registration = new RegistrationFormBuilder()
    .email(EMAIL)
    .firstName(FIRST_NAME)
    .lastName(LAST_NAME)
    .signInProvider(SIGN_IN_PROVIDER)
    .build();

    when(repositoryMock.findByEmail(EMAIL)).thenReturn(null);
    when(repositoryMock.save(isA(User.class))).thenAnswer(new Answer() {
    @Override
    public User answer(InvocationOnMock invocation) throws Throwable {
    Object[] arguments = invocation.getArguments();
    return (User) arguments[0];
    }
    });

    User registered = service.registerNewUserAccount(registration);

    Regards

    • Petri May 13, 2014 @ 18:54

      Hi Cassio,

      This test doesn't persist anything to the database because it uses a mock object instead of the real repository object. If you want to write an integration test for this service method, you should follow these steps:

      1. Run your tests by using the SpringJUnit4ClassRunner class.
      2. Load the required application context configuration classes or files by using the @ContextConfiguration annotation. You need the configuration classes (or files) which configure the service and repository layers of your application.
      3. Inject the tested service to your test class.
      4. Write the integration test.
      • Cassio May 13, 2014 @ 22:00

        thks, now I understood, mocking is used for create objects that simulate the behavior of real objects and for that reason what I need do not work. I tried doing what you said.

        The context configuration is just like this @ContextConfiguration(classes = {PersistentContext.class}).

        When I run the test with: "mvn -Dtest=persistence.UserTest test-compile" I getting the db.drive not found, and then I replace the string db.driver in PersistenceContext.java to com.mysql.jdbc.Driver and ajust anothers cfgs String but now I getting an exception: is requered key com.mysql.jdbc.Driver not found. I think some context classes are missing, do you know which classes I need to specify in @ContextConfiguration?

        Regards

        • Petri May 13, 2014 @ 22:43

          I forgot to mention that you have to configure the used properties file as well. The easiest way to do this is to follow these steps:

          1. Create a new configuration class for your service tests.
          2. Annotate the created class with the @PropertySource annotation and specify the used properties file.
          3. Annotate the configuration class with the @ComponentScan annotation and ensure the Spring container finds your service classes.
          4. Import the PersistenceContext class by annotating the configuration class with the @Configuration annotation.
          5. Configure the PropertySourcesPlaceholderConfigurer bean in the configuration class.
          6. Use the created configuration class when you load the application context of your tests.

          Also, remember to undo the changes made to the PersistenceContext class.

          • Cassio May 13, 2014 @ 23:04

            Ohh Yeahh it's sounds perfect, but I saved my day.. I post the way I did, maybe in the next time I do the same way you recommended.

            Thks again!

        • Cassio Seffrin May 13, 2014 @ 23:00

          Hi Petri,

          Answering myself question, may some else have the same needed.

          Maybe something is not necessary, but it's working like I need:

          I written a new JUnit Class with the following annotations:

          **** PersisTestSpring4J.java *******
          @RunWith(SpringJUnit4ClassRunner.class)
          @ContextConfiguration(classes = {PersistenceContext.class, ExampleApplicationConfig.class, ExampleApplicationContext.class, WebAppContext.class})
          @TransactionConfiguration(defaultRollback = false)
          public class PersistTestSpring4J {

          @Autowired
          private UserService service;

          @Test
          public void insertNewUserInDatabase() {
          System.out.println("starting the JUnit Test insertNewUserInDatabase");

          User u = new User();

          u.setCreationTime(new DateTime());
          u.setFirstName("Cassio");
          u.setLastName("Seffrin");
          u.setEmail("cassioZZseffrin@zzz.zzz");
          u.setPassword("passwd");
          u.setSignInProvider(SocialMediaService.TWITTER);
          u.setRole(Role.ROLE_USER);

          User savedUser = repository.saveAndFlush(u);
          System.out.println("u " + savedUser.getId());
          }

          }
          ******* end class *****

          And then I run it with the follow maven sintaxe:
          #mvn -Dtest=persistenceTests.PersistTestSpring4J test-compile surefire:test -P dev

          The success response is that...

          starting the JUnit Test insertNewUserInDatabase
          Hibernate:
          insert
          into
          User
          (creation_time, modification_time, version, bio, email, first_name, last_name, password, role, sign_in_provider)
          values
          (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
          u 8657
          Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 26.291 sec - in persistenceTests.PersistTestSpring4J
          INFO - nericWebApplicationContext - Closing org.springframework.web.context.support.GenericWebApplicationContext@2779c1cb: startup date [Tue May 13 16:53:51 BRT 2014]; root of context hierarchy
          INFO - erEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
          INFO - BoneCP - Shutting down connection pool...
          INFO - BoneCP - Connection pool has been shutdown.

          Results :

          Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

          [INFO] ------------------------------------------------------------------------
          [INFO] BUILD SUCCESS
          [INFO] ------------------------------------------------------------------------
          [INFO] Total time: 40.066 s
          [INFO] Finished at: 2014-05-13T16:54:15-03:00
          [INFO] Final Memory: 25M/173M
          [INFO] ------------------------------------------------------------------------

          Cheers

          • Petri May 15, 2014 @ 20:38

            Hi,

            I took a closer look and this and noticed that you inject a UserService object to your test class but you call the saveAndFlush() method of a repository (I assume that this is a Spring Data JPA repository). I never write tests to the "standard" repository methods because they have already been tested by the Spring Data team. Also, I spot potential problems if my end-to-end test fails.

            Also, you should add assertions to your tests. At the moment the only way your test can fail is that the entity cannot be inserted to the database. If you write a test which saves something to the database, you should verify that

            1. The correct data is inserted to the database.
            2. If the tested method returns something, you must verify that the correct data is returned.

Leave a Reply