Enterprise Java

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.

Note: 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:

  • FEST-Assert (version 1.4). FEST-Assert 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.easytesting</groupId>
    <artifactId>fest-assert</artifactId>
    <version>1.4</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<String>. 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);
    }
}

Note: 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 FEST-Assert assertions.

Creating Custom Assertions

We can clean up our unit tests by replacing the standard JUnit assertions with custom FEST-Assert assertions. We have to create three 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.

Note: If you are not familiar with FEST-Assert, you should read my blog post which explains how you can create custom assertions by using FEST-Assert, and why you should consider doing this.

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 GenericAssert 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 a Class object which specifies the type of the custom assertion class. Set the value of this constructor argument to ExampleUserDetailsAssert.class.
    2. The second constructor argument is the actual value object. Pass the object given as a constructor argument forward to the constructor of the superclass.
  3. Add a 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 hasFirstName() method to the ExampleUserDetailsAssert class. This method takes a String object as a method parameter and returns an ExampleUserDetailsAssert object. We can implement this method by following these steps:
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the actual first name is equal to the expected first name given as a method parameter.
    3. Return a reference to the ExampleUserDetailsAssert object.
  5. Add a hasId() method to the ExampleUserDetailsAssert class. This method takes a Long object as a method parameter and return an ExampleUserDetailsAssert object. We can implement this method by following these steps:
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the actual id is equal to the expected id given as a method parameter.
    3. Return a reference to the ExampleUserDetailsAssert object.
  6. Add a hasLastName() method to the ExampleUserDetailsAssert class. This method takes a String object as a method parameter and returns an ExampleUserDetailsAssert object. We can implement this method by following these steps:
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the actual last name is equal to the expected last name given as a method parameter.
    3. Return a reference to the ExampleUserDetailsAssert object.
  7. Add a hasPassword() method to the ExampleUserDetailsAssert class. This method takes a String object as a method parameter and returns an ExampleUserDetailsAssert object. We can implement this method by following these steps:
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the actual password is equal to the expected password given as a method parameter.
    3. Return a reference to the ExampleUserDetailsAssert object.
  8. Add a hasUsername() method to the ExampleUserDetailsAssert class. This method takes a String object as a method parameter and returns an ExampleUserDetailsAssert object. We can implement this method by following these steps:
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the actual username is equal to the expected username given as a method parameter.
    3. Return a reference to the ExampleUserDetailsAssert object.
  9. Add a isActive() method to the ExampleUserDetailsAssert class. This method takes no method parameters and it returns an ExampleUserDetailsAssert object.
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the user whose information is stored to the ExampleUserDetails object is active.
    3. Return a reference to the ExampleUserDetailsAssert object.
  10. Add a isRegisteredUser() method to the ExampleUserDetailsAssert class. This method takes no method parameters and it returns an ExampleUserDetailsAssert object.
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the user whose information is stored to the ExampleUserDetails object is a registered user.
    3. Return a reference to the ExampleUserDetailsAssert object.
  11. Add a isRegisteredByUsingFormRegistration() method to the ExampleUserDetailsAssert class. This method returns an ExampleUserDetailsAssert object. We can implement this method method by following these steps:
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the value of the socialSignInProvider field is null.
    3. Return a reference to the ExampleUserDetailsAssert object.
  12. Add a isSignedInByUsingSocialSignInProvider() method to the ExampleUserDetailsAssert class. This method takes a SocialMediaService enum as a method parameter and returns an ExampleUserDetailsAssert object. We can implement this method by following these steps:
    1. Ensure that the actual ExampleUserDetails object is not null by calling the isNotNull() method of the GenericAssert class.
    2. Verify that the value of the socialSignInProvider is equal to the expected SocialMediaService enum given as a method parameter.
    3. Return a reference to the ExampleUserDetailsAssert object.

The source code of the ExampleUserDetailsAssert class looks as follows:

import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class ExampleUserDetailsAssert extends GenericAssert<ExampleUserDetailsAssert, ExampleUserDetails> {

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

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

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

        String errorMessage = String.format(
                "Expected first name to be <%s> but was <%s>",
                firstName,
                actual.getFirstName()
        );

        Assertions.assertThat(actual.getFirstName())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(firstName);

        return this;
    }

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

        String errorMessage = String.format(
                "Expected id to be <%d> but was <%d>",
                id,
                actual.getId()
        );

        Assertions.assertThat(actual.getId())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(id);

        return this;
    }

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

        String errorMessage = String.format(
                "Expected last name to be <%s> but was <%s>",
                lastName,
                actual.getLastName()
        );

        Assertions.assertThat(actual.getLastName())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(lastName);

        return this;
    }

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

        String errorMessage = String.format(
                "Expected password to be <%s> but was <%s>",
                password,
                actual.getPassword()
        );

        Assertions.assertThat(actual.getPassword())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(password);

        return this;
    }

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

        String errorMessage = String.format(
                "Expected username to be <%s> but was <%s>",
                username,
                actual.getUsername()
        );

        Assertions.assertThat(actual.getUsername())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(username);

        return this;
    }

    public ExampleUserDetailsAssert isActive() {
        isNotNull();

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

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

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

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

        return this;
    }

    public ExampleUserDetailsAssert isRegisteredUser() {
        isNotNull();

        String errorMessage = String.format(
                "Expected role to be <ROLE_USER> but was <%s>",
                actual.getRole()
        );

        Assertions.assertThat(actual.getRole())
                .overridingErrorMessage(errorMessage)
                .isEqualTo(Role.ROLE_USER);

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

        String authoritiesCountMessage = String.format(
                "Expected <1> granted authority but found <%d>",
                authorities.size()
        );

        Assertions.assertThat(authorities.size())
                .overridingErrorMessage(authoritiesCountMessage)
                .isEqualTo(1);

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

        String authorityErrorMessage = String.format(
                "Expected authority to be <ROLE_USER> but was <%s>",
                authority.getAuthority()
        );

        Assertions.assertThat(authority.getAuthority())
                .overridingErrorMessage(authorityErrorMessage)
                .isEqualTo(Role.ROLE_USER.name());

        return this;
    }

    public ExampleUserDetailsAssert isRegisteredByUsingFormRegistration() {
        isNotNull();

        String errorMessage = String.format(
                "Expected socialSignInProvider to be <null> but was <%s>",
                actual.getSocialSignInProvider()
        );

        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage(errorMessage)
                .isNull();

        return this;
    }

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

        String errorMessage = String.format(
                "Expected socialSignInProvider to be <%s> but was <%s>",
                socialSignInProvider,
                actual.getSocialSignInProvider()
        );

        Assertions.assertThat(actual.getSocialSignInProvider())
                .overridingErrorMessage(errorMessage)
                .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 GenericAssert 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 a Class object which specifies the type of the custom assertion class. Set the value of this constructor argument to SecurityContextAssert.class.
    2. The second constructor argument is the actual value object. Pass the object given as a constructor argument forward to the constructor of the superclass.
  3. Add a 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 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 GenericAssert 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 loggedInUserIs() method to the SecurityContextAssert class. This method takes a User object 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 GenericAssert 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 loggedInUserHasPassword() method to the SecurityContextAssert class. This method takes a String object 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 GenericAssert 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 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 GenericAssert 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 loggedInUserIsSignedInByUsingSocialProvider() method to the SecurityContextAssert class. This method takes a SocialMediaService enum 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 GenericAssert 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.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;

public class SecurityContextAssert extends GenericAssert<SecurityContextAssert, SecurityContext> {

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

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

    public SecurityContextAssert userIsAnonymous() {
        isNotNull();

        Authentication authentication = actual.getAuthentication();

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

        return this;
    }

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

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

        String errorMessage = String.format("Expected logged in user to be <%s> but was <null>", user);
        Assertions.assertThat(loggedIn)
                .overridingErrorMessage(errorMessage)
                .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();

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

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

        return this;
    }

    public SecurityContextAssert loggedInUserIsRegisteredByUsingNormalRegistration() {
        isNotNull();

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

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

        ExampleUserDetailsAssert.assertThat(loggedIn)
                .isRegisteredByUsingFormRegistration();

        return this;
    }

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

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

        String errorMessage = String.format("Expected logged in user to be <not null> but was <null>");
        Assertions.assertThat(loggedIn)
                .overridingErrorMessage(errorMessage)
                .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 GenericAssert 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 a Class object which specifies the type of the custom assertion class. Set the value of this constructor argument to TestProviderSignInAttemptAssert.class.
    2. The second constructor argument is the actual value object. Pass the object given as a constructor argument forward to the constructor of the superclass.
  3. Add a 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 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 GenericAssert class.
    2. Ensure that the actual TestProviderSignInAttempt object created no connections.
    3. Return a reference to the TestProviderSignInAttemptAssert object.
  5. Add a createdConnectionForUserId() method to the created class. This method takes a String object 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 GenericAssert 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.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
import org.springframework.social.connect.web.TestProviderSignInAttempt;

public class TestProviderSignInAttemptAssert extends GenericAssert<TestProviderSignInAttemptAssert, TestProviderSignInAttempt> {

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

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

    public TestProviderSignInAttemptAssert createdNoConnections() {
        isNotNull();

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

        return this;
    }

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

        String error = String.format(
                "Expected that connection was created for user id <%s> but found none.",
                userId
        );

        Assertions.assertThat(actual.getConnections())
                .overridingErrorMessage(error)
                .contains(userId);

        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.

Note: 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);
    }
}

Note: 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. Create a new RegistrationForm object by using the RegistrationFormBuilder class. Set the value of the signInProvider field.
  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. Convert the form object into url encoded bytes and set the outcome of the conversion into the body of the request.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set the form object to the HTTP session.
  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. 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 ‘twitter’.
  8. Ensure that the model attribute called ‘user’ has field errors in email, firstName, and lastName fields.
  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 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();

        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .signInProvider(SocialMediaService.TWITTER)
                .build();

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", userAccountData)
        )
                .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. Create a new RegistrationForm object by using the RegistrationFormBuilder class. Set the values of email, firstName, lastName, and signInProvider fields.
  3. Configure the UserService mock to throw a DuplicateEmailException when its registerNewUserAccount() method is called and the form 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. Convert the form object into url encoded bytes and set the outcome of the conversion into the body of the request.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set the form object to the HTTP session.
  5. Verify that the HTTP status code 200 is returned.
  6. Ensure that the name of the rendered view is ‘user/registrationForm’.
  7. Ensure that the request is forwarded to url ‘/WEB-INF/jsp/user/registrationForm.jsp’.
  8. 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 ‘twitter’.
  9. Ensure that the model attribute called ‘user’ has field error in email field.
  10. Verify that the current user is not logged in.
  11. Ensure that no connections were created by using the TestProviderSignInAttempt object.
  12. Verify that the registerNewUserAccount() method of the UserService mock was called once and that the RegistrationForm object was given as a method parameter.
  13. Verify that the other methods of the UserService interface weren’t invoked during the test.

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_SocialSignInAndEmailExist_ShouldRenderRegistrationFormWithFieldError() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .providerId("twitter")
                .userProfile()
                    .email("john.smith@gmail.com")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .email("john.smith@gmail.com")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();

        when(userServiceMock.registerNewUserAccount(userAccountData)).thenThrow(new DuplicateEmailException(""));

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", userAccountData)
        )
                .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();

        verify(userServiceMock, times(1)).registerNewUserAccount(userAccountData);
        verifyNoMoreInteractions(userServiceMock);
    }
}

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 RegistrationForm object by using the RegistrationFormBuilder class. Set the values of email, firstName, lastName, and signInProvider fields.
  3. Create a new User object by using the UserBuilder class. Set the values of id, email, firstName, lastName, and signInProvider fields.
  4. Configure the UserService mock object to return the created User object when its registerNewUserAccount() method is called and the RegistrationForm object is given as a method parameter.
  5. 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. Convert the form object into url encoded bytes and set the outcome of the conversion into the body of the request.
    3. Set the created TestProviderSignInAttempt object to the HTTP session.
    4. Set the form object to the HTTP session.
  6. Verify that the HTTP status code 302 is returned.
  7. Ensure that the request is redirected to url ‘/’.
  8. Verify that the created user is logged in by using Twitter.
  9. Verify that the TestProviderSignInAttempt object was used to created a connection for a user with email address ‘john.smith@gmail.com’.
  10. Verify that the registerNewUserAccount() method of the UserService mock was called once and that the form object was given as a method parameter.
  11. Verify that the other methods of the UserService mock weren’t invoked during the test.

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.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();

        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .email("john.smith@gmail.com")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();

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

        when(userServiceMock.registerNewUserAccount(userAccountData)).thenReturn(registered);

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .sessionAttr("user", userAccountData)
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(redirectedUrl("/"));

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

        verify(userServiceMock, times(1)).registerNewUserAccount(userAccountData);
        verifyNoMoreInteractions(userServiceMock);
    }
}

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.
 

Petri Kainulainen

Petri is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button