In this blog post we will concern ourselves with unit testing of classic 3-layer business applications. We will assume that all business logic lives in services and components, that these services operate on entities that are stored and retrieved from relational database, and that these entities doesn’t contain any logic. Moreover we assume usage of DTO (Data Transfer Object) to pass data between GUI and application services.
Dependency Injection is indispensable when it comes to unit testing of modern business applications. Without DI you are forced to write slow and difficult to maintain integration tests instead of unit tests. If you don’t know what DI is or if you don’t used it before please read articles on Wikipedia and Martin Fowler site, and return here after you are comfortable with both idea and usage of DI.
Now when we are ready to start, we will follow Confucius advice:
By three methods we may learn wisdom: First, by reflection, which is noblest; Second, by imitation, which is easiest; and third by experience, which is the bitterest.
Confucius
and learn by imitation,
by observing how we may unit test UserService
component.
We will use popular JUnit unit testing framework
with Mockito mocking library.
UserService
component
UserService
implements following business requirements:
- Users forgot their passwords from time to time, application should provide a way to reset forgotten passwords.
- To reset their passwords users must provide email address they use to login to our system.
- If provided email address does not belong to any user, application should do nothing. Otherwise application should generate unique password reset token and send to provided email address message with link to reset password form. Link should contain reset password token. In both cases application should show to user success message.
- Password reset tokens should be unique. Tokens should be hard to guess or enumerate (no numbers here). Token may be used only once to reset password. If we want to reset password again we need a new token. Token is valid for 24 hours starting from the date it was created, after 24 hours token cannot be used to change password.
- When user open reset password link in her browser it should be presented with a form that allows to enter a new password. After clicking OK, application should validate token used in link, and if it is still valid application should change user password and make token invalid. Then application should send password change confirmation message to user. In case of expired token application should show warning to user.
WARNING Before implementing real password reset feature please read Everything you ever wanted to know about building a secure password reset feature.
Now when we understand business requirements we may attempt to implement
UserService
component:
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final NotificationService notificationService;
private final DateTimeProvider dateTimeProvider;
private final CryptoService cryptoService;
public UserServiceImpl(
UserRepository userRepository,
DateTimeProvider dateTimeProvider,
CryptoService cryptoService,
NotificationService notificationService)
{
this.userRepository = requireNonNull(userRepository);
this.notificationService = requireNonNull(notificationService);
this.dateTimeProvider = requireNonNull(dateTimeProvider);
this.cryptoService = requireNonNull(cryptoService);
}
@Override
public void startResetPasswordProcess(String userEmailAddress) {
User user = userRepository.findByEmailAddress(userEmailAddress);
if (user == null)
return;
UUID token = UUID.randomUUID();
LocalDateTime tokenValidityEndDate =
dateTimeProvider.now().plusDays(1);
user.setResetPasswordToken(token);
user.setResetPasswordTokenValidityEndDate(
tokenValidityEndDate);
ResetPasswordNotificationData notificationData =
new ResetPasswordNotificationData(
user.getEmail(),
token,
tokenValidityEndDate);
notificationService
.sendResetPasswordNotification(notificationData);
}
@Override
public void finishResetPasswordProcess(
String userEmailAddress,
String newPassword,
UUID resetPasswordToken)
{
User user = userRepository.findByEmailAddress(userEmailAddress);
if (user == null)
return;
if (user.getResetPasswordToken() == null)
return;
if (!user.getResetPasswordToken().equals(resetPasswordToken))
return;
if (user.getResetPasswordTokenValidityEndDate()
.isBefore(dateTimeProvider.now()))
return;
user.setResetPasswordToken(null);
user.setResetPasswordTokenValidityEndDate(null);
String newPasswordHash = cryptoService.sha1(newPassword);
user.setPasswordHash(newPasswordHash);
notificationService
.sendPasswordChangedConfirmation(user.getEmail());
}
}
UserService
operates on the following User
entity:
public class User {
private Long id;
private String email;
private String passwordHash;
private UUID resetPasswordToken;
private LocalDateTime resetPasswordTokenValidityEndDate;
// getter, setter, etc.
}
And requires four other components to work, namely: UserRepository
,
NotificationService
, DateTimeProvider
and CryptoService
:
public interface UserRepository {
User findByEmailAddress(String emailAddress);
}
public interface NotificationService {
void sendResetPasswordNotification(
ResetPasswordNotificationData data);
void sendPasswordChangedConfirmation(String email);
}
public interface DateTimeProvider {
LocalDateTime now();
}
public interface CryptoService {
String sha1(String input);
}
DateTimeProvider
dependency was introduced solely for the purpose
of easier unit testing, as we will find out later.
Design of UserService
follows principles of DI, component advertises
all dependencies it needs as constructor parameters.
DI containers may then use constructor based dependency injection to
provide implementations of these dependencies.
Right now we have only implemented UserService
, UserRepository
and
other dependencies are not implemented yet.
Nevertheless with usage of stubs and
mocks we may test UserService
implementation right now.
Writing tests for startResetPasswordProcess
By convention we should put tests for ComponentName
into ComponentNameTest
class. For example tests for UserServiceImpl
should be put
into UserServiceImplTest
class.
When you use Maven you should put your test class in
the same package that contains tested component.
For example UserServiceImpl
is part of io.mc.letsmock.demo
package, so UserServiceImplTest
should also belong to io.mc.letsmock.demo
package.
With Maven the only difference between application code and test code is the
directory in which code resides. Application code will be in src/main/java
directory and test code will be in src/test/java
directory:
.
`-- src
|-- main
| `-- java
| `-- io
| `-- mc
| `-- letsmock
| `-- demo
| |-- UserServiceImpl.java
| `-- UserService.java
`-- test
`-- java
`-- io
`-- mc
`-- letsmock
`-- demo
`-- UserServiceImplTest.java
Another popular
convention used with e.g. Ant is to put applicaiton code into
mycompany.productA.xx.yy
package and test code
into mycompany.productA.test.xx.yy
package.
The important thing here is that team should choose one particular convention how to name tests and where to put test classes and stick to it. If you use Maven I strongly encourage using conventions that I described above.
Naming tests
After reading requirements we come to conclusion that we need the following
test cases to be sure that startResetPasswordProcess
method works:
- When we couldn’t find
User
with specified email address, component should do not nothing, in particular is should not crash - When there is
User
with specified email address, component should setresetPasswordToken
andresetPasswordTokenValidityEndDate
fields onUser
instance to respectively newly generated token, andLocalDateTime
instance that represent point in time 24 hours later than now. - When there is
User
with specified email address, component should send message with token to user usingNotificationService
.
Notice that each of these test cases test only single thing, this is very important if we want to have clean and independent tests. As method should do only one thing, test should test only one “outcome” of a method. When you gain more experience you may want to relax this rule, but if you just started unit testing you should stick with it for some time.
There are two schools when it comes to naming test methods, first school teaches that test name should consists of three parts:
@Test
void scenario_conditions_outcome()
scenario
is the thing that we want to test, this in most cases
will be the name of the method that we want to test.
conditions
describe the state of program that tested method
may expect when it is called. outcome
is the state of the program
that we expect after tested method returns.
To give you a feeling how this naming scheme works here
are some dummy tests for Java +
operator:
void plusOperator_given1And5_returns6()
void plusOperator_given1AndMinus7_returnsMinus6()
void plusOperator_whenResultIsGreaterThanMAXINT_wrapsResultsUsingMod2Arithmetic()
The second school took inspiration for thier naming scheme from
BDD
movement.
This school advices that
test names should consists of full sentences that describe both
conditions and outcome of tested method.
Names for +
operator tests following this
scheme looks like:
void _1_plus_5_should_return_6()
void _1_plus_minus_7_should_return_minus_6()
void when_result_of_addition_is_greater_than_MAXINT_plus_operator_should_wrap_result_using_mod_2_arithmetic()
As you can see test names generated using this approach can be quite verbose at times. Verbosity of this schema is it great advantage because the main purpose of test name is to tell you what exactly is not working when given test fails.
I prefer first school with scenario/conditions/outcome division of test name, so I will use it exclusively in the rest of this post.
Returning to startResetPasswordProcess
we should create three tests:
void startResetPasswordProcess_givenEmailNotBelongingToAnyUser_doesNothing()
void startResetPasswordProcess_givenEmailOfExistingUser_generatesToken()
void startResetPasswordProcess_givenEmailOfExistingUser_sendsNotificationToUser()
These test names are not as descriptive as they may be, but are close to what you may expect in average enterprise application.
Anatomy of test method
Our first test will check that startResetPasswordProcess
won’t throw
exceptions when called with email address of non-existing user.
But to test UserServiceImpl
class we must create it’s instance and this
will require providing all necessary dependencies.
Fortunately we may use Mockito library to
to generate
dummy implementations of UserRepository
, NotificationService
and others. By default these dummy implementations do nothing and
are similar to handcrafted test stubs like:
public class UserRepositoryStub implements UserRepository {
@Override
public User findByEmailAddress(String emailAddress) {
return null;
}
}
But we will soon see that we can instruct Mockito to return values from these stubs or to check if a given method was called on dummy object.
Below you can see our first test code:
@Test
public void startResetPasswordProcess_givenEmailNotBelongingToAnyUser_doesNothing() {
// arrange
UserRepository userRepository = mock(UserRepository.class);
DateTimeProvider dateTimeProvider = mock(DateTimeProvider.class);
CryptoService cryptoService = mock(CryptoService.class);
NotificationService notificationService = mock(NotificationService.class);
when(userRepository.findByEmailAddress("unknown@example.com"))
.thenReturn(null);
UserServiceImpl userService = new UserServiceImpl(
userRepository,
dateTimeProvider,
cryptoService,
notificationService);
// act
userService.startResetPasswordProcess("unknown@example.com");
// assert
verify(notificationService, never())
.sendResetPasswordNotification(any());
verify(notificationService, never())
.sendPasswordChangedConfirmation(anyString());
}
Before we dig into details let’s look at this test from high level point of view. Almost every test method can be divided into three parts called Arrange-Act-Assert or Given-When-Then. I used comments to signify when each of these parts start. Each of these parts has different purpose. In Arrange part we must create instance of tested component and all necessary test data and also we must set up Mockito mocks. If we may compare test method to theater play, then Arrange is like preparing scene, costumes and lights.
The first four lines of our test Arrange section are responsible for
creating dummy implementations of UserServiceImpl
dependencies:
UserRepository userRepository = mock(UserRepository.class);
DateTimeProvider dateTimeProvider = mock(DateTimeProvider.class);
CryptoService cryptoService = mock(CryptoService.class);
NotificationService notificationService = mock(NotificationService.class);
Then we set up one of our stub objects:
when(userRepository.findByEmailAddress("unknown@example.com"))
.thenReturn(null);
Here we instruct Mockito that dummy implementation generated for
UserRepository::findByEmailAddress
should return null
when called with "unknown@example.com"
string.
But to be honest these lines are redundant because
method stubs generated by Mockito by default return null
for
reference types, default values for primitives and empty collections
for methods returning collections.
Still I leave them because they
make purpose of our test more evident.
After Arrange section we have an Act section. Again (so many AAAAs) returning to our theater play analogy (another A, no pun intended) Act section is like the actual play, we invoke tested component and let the code be alive. Act part is usually very short, in most cases it consists of single line of code:
userService.startResetPasswordProcess("unknown@example.com");
The last section in the test method is Assert when we want to check results produced by tested code. In our test we check two assumptions:
- Invocation of
startResetPasswordProcess
does not throw any exceptions. This is tested implicitly by JUnit - test fails when test method throwns exception. Since our test passes we are certain thatstartResetPasswordProcess
doesn’t throw any. - We want to be certain that no notification was send to provided email address
so we asses with the help of Mockit that none of the methods on
NotificationService
was called.
Mockito verification syntax is a bit unintuitive, so let’s take a closer look at one of our assertions:
verify(notificationService, never())
.sendResetPasswordNotification(any());
We use verify
to tell Mockit that we want to preform verification.
never()
means that we expect that method was not called on dummy object.
Then we specify method that should not be called,
in our case sendResetPasswordNotification
.
We pass any()
as method parameter to tell Mockito that method
should just not be called, and that we don’t care about parameters
that was passed to it. Mockito is quite flexible here
and we may for example verify that method should not be called with given
set of parameters but must be called with another. We may also replace
never()
with one of several predicates like e.g. times(2)
to assert that
method was called twice.
Testing with assertions
Our first test assured us that startResetPasswordProcess
works correctly
with email address of unknown user. Now it is time to check if
it also works correctly given email addresses of existing user.
Our second test will check if given valid email address UserServiceImpl
generates a new token for User
and sets it expiry date correctly.
We also will check that user password is not altered in any way by
starting reset password process (it should change only when we finish
password change process).
Here is our second test code:
@Test
public void startResetPasswordProcess_givenEmailOfExistingUser_generatesToken() {
// arrange
UserRepository userRepository = mock(UserRepository.class);
DateTimeProvider dateTimeProvider = mock(DateTimeProvider.class);
CryptoService cryptoService = mock(CryptoService.class);
NotificationService notificationService = mock(NotificationService.class);
UserServiceImpl userService = new UserServiceImpl(
userRepository,
dateTimeProvider,
cryptoService,
notificationService);
User joe = new User();
joe.setEmail("joe@example.com");
joe.setResetPasswordToken(null);
joe.setResetPasswordTokenValidityEndDate(null);
joe.setPasswordHash("old-password-hash");
when(userRepository.findByEmailAddress("joe@example.com"))
.thenReturn(joe);
when(dateTimeProvider.now())
.thenReturn(LocalDateTime.of(2017,3,10, 0,0));
// act
userService.startResetPasswordProcess("joe@example.com");
// assert
assertThat(joe.getResetPasswordToken())
.isNotNull();
assertThat(joe.getResetPasswordTokenValidityEndDate())
.isEqualTo(LocalDateTime.of(2017,3,11, 0,0));
assertThat(joe.getPasswordHash())
.withFailMessage("Password should not be changed")
.isEqualTo("old-password-hash");
}
Arrange section of our second test does not differ much from Arrange
section of our first test, except that we added code for creation
of an User
instance. Notice that we populate User
with carefully
chosen data that will allow us to test startResetPasswordProcess
implementation easily e.g. we set both resetPasswordToken
and
resetPasswordTokenValidityEndDate
to null
.
Then we instruct Mockito to return our User
instance when we
ask UserRepository
for user with joe@example.com
email address:
when(userRepository.findByEmailAddress("joe@example.com"))
.thenReturn(joe);
We should strive to make our unit tests as much deterministic as possible.
Unit test that sometimes fails without a reason is burden rather than
a benefit, and should be either fixed or removed.
To make unit more robust we should avoid depending on external input
like information about current time. To facilitate that I decided to
introduce DateTimeProvider
component with sole purpose of making
unit tests more predictable. With small help of Mockito we are now
masters of time:
when(dateTimeProvider.now())
.thenReturn(LocalDateTime.of(2017,3,10, 0,0));
After all this preparations, we are now free to invoke startResetPasswordProcess
and check it’s outcome. In unit tests we mainly use assertions to check
correctness of tested code. JUnit already comes with handy assertion library
that we may use like this:
import static org.junit.Assert.*;
assertNotNull(joe.getResetPasswordToken());
assertEquals("old-password-hash", joe.getPasswordHash());
When JUnit assertion fails it throws AssertionError
, test fails and
we get error similar to:
java.lang.AssertionError
at org.junit.Assert.fail(Assert.java:86)
at org.junit.Assert.assertTrue(Assert.java:41)
at org.junit.Assert.assertNotNull(Assert.java:712)
at org.junit.Assert.assertNotNull(Assert.java:722)
at io.mc.letsmock.demo.UserServiceImplTest.startResetPasswordProcess_givenEmailOfExistingUser_generatesToken(UserServiceImplTest.java:113)
Unfortunately error messages generated by JUnit assertions are not always the best way to find out what went wrong with failing tests. JUnit assertions are also cumbersome to use at times. From these and other reasons I prefer to use assertions from assertJ library and I used them when I wrote our second test:
import static org.assertj.core.api.Assertions.assertThat;
assertThat(joe.getResetPasswordToken())
.isNotNull();
assertThat(joe.getResetPasswordTokenValidityEndDate())
.isEqualTo(LocalDateTime.of(2017,3,11, 0,0));
assertThat(joe.getPasswordHash())
.withFailMessage("Password should not be changed")
.isEqualTo("old-password-hash");
Here we check three things:
- New password reset token was generated and assigned to
resetPasswordToken
property - Expiry date of password reset token was set correctly (token should be valid for the next 24 hours)
- Current user password was not changed
Notice also that we use withFailMessage
to provide additional
information in case our third assertion fails.
Without withFailMessage
we would get following error:
org.junit.ComparisonFailure:
Expected :"old-password-hash"
Actual :"xxx"
With withFailMessage
we get:
java.lang.AssertionError: Password should not be changed
Merciless refactoring
Right now both of our tests pass, but we see a lot of code duplication
between them. Now it is a good time to extract common parts of both
tests into setUp
method and to create some fields in our test class:
public class UserServiceImplTestAfterRefactoring {
private UserRepository userRepository;
private DateTimeProvider dateTimeProvider;
private CryptoService cryptoService;
private NotificationService notificationService;
private UserServiceImpl userService;
@Before
public void setUp() {
userRepository = mock(UserRepository.class);
dateTimeProvider = mock(DateTimeProvider.class);
cryptoService = mock(CryptoService.class);
notificationService = mock(NotificationService.class);
userService = new UserServiceImpl(
userRepository,
dateTimeProvider,
cryptoService,
notificationService);
}
@Test
public void startResetPasswordProcess_givenEmailNotBelongingToAnyUser_doesNothing() {
// arrange
when(userRepository.findByEmailAddress("unknown@example.com"))
.thenReturn(null);
// act
userService.startResetPasswordProcess("unknown@example.com");
// assert
verify(notificationService, never())
.sendResetPasswordNotification(any());
verify(notificationService, never())
.sendPasswordChangedConfirmation(anyString());
}
@Test
public void startResetPasswordProcess_givenEmailOfExistingUser_generatesToken() {
// arrange
User joe = Fixtures.userJoe();
when(userRepository.findByEmailAddress("joe@example.com"))
.thenReturn(joe);
when(dateTimeProvider.now())
.thenReturn(LocalDateTime.of(2017,3,10, 0,0));
// act
userService.startResetPasswordProcess("joe@example.com");
// assert
assertThat(joe.getResetPasswordToken())
.isNotNull();
assertThat(joe.getResetPasswordTokenValidityEndDate())
.isEqualTo(LocalDateTime.of(2017,3,11, 0,0));
assertThat(joe.getPasswordHash())
.withFailMessage("Password should not be changed")
.isEqualTo("old-password-hash");
}
}
After refactoring both dependencies and tested components are
now stored in fields of test class.
JUnit calls any method annotated by @Before
before executing each
of test methods contained in test class.
This makes setUp
method suitable place to initialize fields
that will be used by many tests.
Now you may be tempted to move initialization code to the test class constructor, but don’t do that. Unit tests should be independent of each other. One of the worst sins when writing unit tests is to write a test that depends on some data created by other test methods. Such incorrect test may pass when we ran all tests but will fail when run it alone. This is one of the worst things that may happen when writing unit tests, and clearly shows that we do something wrong. Instead every test should create it’s own test data and should use fresh Mockito stubs. Later you will appreciate this independence of tests when you will try to run tests in parallel.
Unit tests often require some dummy data, instead of creating the
same object again and again in various tests we should group them
into library of test objects. Such library of test data is often called
a fixture. Since I expected that we will need User
instance in other
test, I extracted code that created dummy user into Fixtures
class:
public class Fixtures {
public static User userJoe() {
User joe = new User();
joe.setEmail("joe@example.com");
joe.setPasswordHash("old-password-hash");
joe.setResetPasswordToken(null);
joe.setResetPasswordTokenValidityEndDate(null);
return joe;
}
}
Terminology
When reading about unit testing you may encounter terms fake, mock and stub.
Fake is any object that is used only by test code, fake may be implemented as
concrete class like UserRepositoryStub
or as anonymous class generated at runtime.
In this last category we find all dummy implementations generated by Mockito.
Fakes can be divided into two groups stubs and mocks. Form practical point of
view we use mock to test interactions, and stubs to provide dummy data or
do-nothing implementation. We used NotificationService
as a mock in our first
test because we used it to assert that no interaction took place (no message was
send to user). On the other hand all dummy implementations generated
by Mockito for UserRepository
, DateTimeProvider
etc. are examples of stubs.
More information about difference between mocks and stubs can be found in Martin Fowler article Mocks aren’t stubs.
Code coverage
When unit testing it is important to test all execution paths in our code.
Sometimes we may be convinced that we covered all corner cases only to
find out (usually in production) that we overlooked testing some obscure conditions.
In such situation after fixing bug, we should add missing test.
But we could do better, almost any popular Java IDE can measure
and show us code coverage of tested component. IDE can usually highlight
in red lines that were not tested, for example here how it looks like in IntelliJ:
Greenish bar next to line number tells us that line of code was executed
when running unit tests (being executed doesn’t automatically mean that
the line of code is well tested, it is only a heuristic). On the other hand
red bar tells that lines of code was not reached by tests and certainly is not tested.
When we are at it, you may heard that the higher code coverage the better. Having 100% code coverage is impossible in any reasonable sized enterprise application. There is an ongoing debate about how much code coverage is enough. For me code coverage is just a tool that I use to check that I tested all execution paths in code and nothing more.
It’s your turn now
Right now we have only two tests for our UserServiceImpl
component,
of course it is not enough to assure us that all of the business requirements
were fulfilled. When I tested UserServiceImpl
I wrote seven more tests:
startResetPasswordProcess_givenEmailOfExistingUser_sendsNotificationToUser
finishResetPasswordProcess_noUserHasSpecifiedEmail_doesNothing
finishResetPasswordProcess_userHasNoTokenSet_doesNothing
finishResetPasswordProcess_tokenExpired_doesNothing
finishResetPasswordProcess_tokenNotMatch_doesNothing
finishResetPasswordProcess_validToken_changesPassword
finishResetPasswordProcess_validToken_sendsConfirmationToUser
and only now I am certain that UserServiceImpl
works correctly.
You may find source code for all these tests (with other goodies) HERE. But before you look at what I wrote, please try to implement these tests yourself and then compare your code with mine. I am certain that you will learn more this way.
Thanks for reading. If you liked this post please start my Github repository. And see you soon again!