Item 6: Cover all scenarios

By Ashley Waldron

All of the source code for this series can be found in this repository. Specifically, the code used throughout this item can be found here and its corresponding test classes found here.

….Not lines…..scenarios

In item 1 we identified at least 4 scenarios that we need to cover. We’ve already got the default happy path scenario done so now we’ll cover the rest of them. I always create a default happy path scenario as the first test for a method or class because once I’ve got all the mock setups created I can reuse most of it in the following tests since the default happy path goes through to the end of the method there’s a good chance that it hits all or most of the mocks along the way. Plus if needed, I can probably copy most of the test object setup and asserts to other tests too and tweak them.

The second scenario we’ll test is when the shouldReceiveMarketingEmails flag is set to false [See Item6UserServiceTest.createSuccessWithShouldReceiveMarketingEmailsFalseItem6()]:

Item6UserServiceTest.createSuccessWithShouldReceiveMarketingEmailsFalseItem6()
@Test
void createSuccessWithShouldReceiveMarketingEmailsFalseItem6() {
    given(userRepository.save(userEntityArgumentCaptor.capture())).willReturn(mockSavedUserEntity);
    given(employeeNumberGenerator.generate(TEST_REGION)).willReturn(TEST_EMPLOYEE_NUMBER);
    createUserProfileRequest.shouldReceiveMarketingEmails(false);
 
    CreateUserProfileResponse createUserProfileResponse = item5UserService.create(createUserProfileRequest);
 
    assertThat(createUserProfileResponse.getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
    assertThat(createUserProfileResponse.getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
    assertThat(createUserProfileResponse.getRegion(), is(equalTo(TEST_REGION)));
    assertThat(createUserProfileResponse.getId(), is(equalTo(TEST_UUID)));
 
    assertThat(userEntityArgumentCaptor.getValue().getEmployeeNumber(), is(equalTo(TEST_EMPLOYEE_NUMBER)));
    assertThat(userEntityArgumentCaptor.getValue().getEmailAddress(), is(equalTo(TEST_EMAIL_ADDRESS)));
    assertThat(userEntityArgumentCaptor.getValue().getRegion(), is(equalTo(TEST_REGION)));
    assertThat(userEntityArgumentCaptor.getValue().getId(), matchesPattern(UUID_PATTERN));
 
    then(eventBroadcaster).should().broadcast(EventType.USER_REGISTRATION, mockSavedUserEntity);
    then(emailService).shouldHaveNoInteractions();
}

The only way this scenario behaves differently from the default happy path scenario is that if the shouldReceiveMarketingEmail flag is false the emailService.registerSubscriber() method is not called. We need to make sure that this is true by using the shouldHaveNoInteractions() verification on line 105. If there were other methods that happened to be called on the EmailService class during this test we would use then(emailService).should(never()).registerSubscriber(mockSavedUserEntity); but since there aren’t we can use then(emailService).shouldHaveNoInteractions() as it’s even more strict.

The next scenario we’ll code is testing what happens when the region is null:

Item6UserServiceTest.createWithNullRegionItem6()
@Test
void createWithNullRegionItem6() {
    createUserProfileRequest.setRegion(null);
 
    RuntimeException thrownException = assertThrows(RuntimeException.class, () -> item5UserService.create(createUserProfileRequest));
 
    assertThat(thrownException.getMessage(), is(equalTo("Region must be less than 30 characters in length")));
}

Don’t forget to assert mock interactions (or lack thereof) in exception throwing scenarios

Pretty simple test and it might look like we’re done here but we’re not. We’ve asserted everything that gets returned from the method (or thrown out of it in this case) but we haven’t made sure that the code didn’t do something else that it wasn’t supposed to before it finally finished executing. One way we need to do this is by making sure that in this case none of the external classes were interacted with. The example we have doesn’t make the need to do stuff like this very obvious. But imagine that there’s a try/catch somewhere in the method which catches some exception that gets thrown, does some stuff and then rethrows the exception (or throws some other new exception). You want to make sure that when that exception occurs that the code only does what it’s supposed to and no more before rethrowing it.

Let’s update the test to make sure no mocks have been interacted with, because that’s how the current code should behave in this scenario [See Item6UserServiceTest.createWithNullRegionItem6()].

Item6UserServiceTest.createWithNullRegionItem6()
    @Test
    void createWithNullRegionItem6() {
        createUserProfileRequest.setRegion(null);
 
        RuntimeException thrownException = assertThrows(RuntimeException.class, () -> item5UserService.create(createUserProfileRequest));
 
        assertThat(thrownException.getMessage(), is(equalTo("Region must be less than 30 characters in length")));
        then(employeeNumberGenerator).shouldHaveNoInteractions();
        then(userRepository).shouldHaveNoInteractions();
        then(emailService).shouldHaveNoInteractions();
        then(eventBroadcaster).shouldHaveNoInteractions();
    }

Technically speaking we can do more to make this unit test more robust, but this is definitely good enough. Anything more is overkill, which we might address in a future item for fun.

The next scenario is when the region is more than 30 characters [See Item6UserServiceTest.createWith31CharacterRegionItem6()]:

Item6UserServiceTest.createWith31CharacterRegionItem6()
    @Test
    void createWith31CharacterRegionItem6() {
        createUserProfileRequest.setRegion("ThisRegionIs31CharactersLongThi");
 
        RuntimeException thrownException = assertThrows(RuntimeException.class, () -> item5UserService.create(createUserProfileRequest));
 
        assertThat(thrownException.getMessage(), is(equalTo("Region must be less than 30 characters in length")));
        then(employeeNumberGenerator).shouldHaveNoInteractions();
        then(userRepository).shouldHaveNoInteractions();
        then(emailService).shouldHaveNoInteractions();
        then(eventBroadcaster).shouldHaveNoInteractions();
    }

Now finally, let’s run the full test class with our code coverage tool and see if we’ve gotten to 100% code coverage.

….Aaaaand……the code coverage tool shows 0% coverage!!! Don’t worry the code is actually at 100% coverage and we’re done with these tests. But we’ll see in the next item what’s going on with our code coverage tool.