Unit tests serve to verify individual components, mostly on business logic level, and to ensure that components perform as expected. In Spring Webflux apps this level is a level of services. Here we want to check that the defined flow is executed properly. To do this we usually use a testing framework, like JUnit and abstract (isolate) component’s dependencies with mocks to control the service under test.

In this article I would like to continue a series of posts initially designed about two-factor authentication in Spring reactive APIs. I think, that it is logically to provide a guide about testing of such components, as the reactive testing brings differencies and difficulties, many developers are not aware about.

What is unit testing?

Unit testing is a level of software testing, when we test individual software’s components in isolation. For example, we have UserService, that may have various connected dependencies: UserRepository component to connect with a datasource, EmailClient to communicate with remote API that will send confirmation emails. For unit testing, we isolate UserService and then mock external dependencies.

The unit testing offers us a number of benefits, to name few:

  • It increases our confidence, when we change code. If unit tests are good written and if they are run every time any code is changed, we can notice any failures, when we introduce new features
  • It serves as documentation. Certanly, documenting your code includes several instruments, and unit testing is one of them – it describes an expected behaviour of your code to other developers.
  • It makes your code more reusable, as for good unit tests, code components should be modular.

These advantages are just few of numerous, that are provided us by unit testing. Now, when we defined what is unit testing and why do we use it, we can move to concrete examples.

Service under test

In this post I want to conclude a “trilogy” of posts devoted to Webflux two-factor authetnication series. We will do unit testing of AuthServiceImpl component, which is a typical Spring reactive service – a component, that is responsible for business logic. We will take as an example signup flow.

As a signup proccess can provide two possible outcomes – successful signup or denied result, we expect two possible test scenarios. Note, that I use AssertJ library in this post to write assertions.

Scenario 1: Successful signup

In the previous post we defined that signup is completed, if no user with requested email was found. Let define a steps, that we need to accomplish with Project Reactor tests:

  1. Define mock data: in our case SignupResponse and User entites
  2. Create producers (also called sources in some publications) – Mono or Flux.
  3. Configure mocks: in this example we have two dependencies – repository and token manager
  4. Create StepVerifier: this component provides a declarative way of creating a verifiable script for an async Publisher sequence, by expressing expectations about the events that will happen upon subscription.

Now, let implement this in code. Take a look on the snippet below:

@Test
void signupSuccessTest(){
    final String email = "john.doe@mail.com";
    final String secretKey = "secretkey";
    final String userId = "userId";
    final String token = "token";

    SignupRequest request = new SignupRequest();
    request.setEmail(email);
    request.setPassword("secret");

    User user = new User();
    user.setEmail(email);
    user.setSecretKey(secretKey);
    user.setUserId(userId);

    Mono<User> userSource = Mono.just(user);

    Mockito.when(repository.findByEmail(email)).thenReturn(Mono.empty());
    Mockito.when(totpManager.generateSecret()).thenReturn(secretKey);
    Mockito.when(tokenManager.issueToken(Mockito.anyString())).thenReturn(token);
    Mockito.when(repository.save(Mockito.any(User.class))).thenReturn(userSource);

    StepVerifier.create(service.signup(request))
            .assertNext(result -> Assertions.assertThat(result)
                    .hasFieldOrPropertyWithValue("userId", userId)
                    .hasFieldOrPropertyWithValue("token", token)
                    .hasFieldOrPropertyWithValue("secretKey", secretKey))
            .verifyComplete();
}

If you followed all mentioned steps, the result is passing:

Graph 1. signupSuccessTest() expected output

Another scenario we will observe is failed signup (or denied). Let have a look on it in the next section.

Scenario 2: Signup denied

This case assumes that we can’t create a new user account, because the entity with requested email address is already in the database. Recall, that in the previous post we did error handling, and specified that in such outcome, service throws an exception. That means, that instead of asserting a result value, like we did in the last section, we need to check an error.

For that, StepVerifier supplies a method expectError(). It has two version – one without arguments expects any error, and the second version provides a way to expect a specific error. In our case it is AlreadyExistsException.

Take a look on the code implementation:

@Test
void signupDeniedTest(){
    final String email = "john.doe@mail.com";

    SignupRequest request = new SignupRequest();
    request.setEmail(email);

    User user = new User();
    user.setEmail(email);

    Mono<User> userSource = Mono.just(user);

    Mockito.when(repository.findByEmail(email)).thenReturn(userSource);

    StepVerifier.create(service.signup(request))
        .expectError(AlreadyExistsException.class);
}

Again, if everything was correct, the desired outcome is passing:

Graph 2. signupDeniedTest() expected output

We will not do login flow’s unit testing in this post, as it follows same logic. You can do it yourself, once you read about login process. You can expect a complete source code for this post in this github repository.

References

  • Khanh Nguyen Write Unit Test for Project Reactor using class StepVerifier (2020) access here
  • Yuri Mednikov Intro to Unit Testing in Java With JUnit5 Library (2019) DZone access here