Keep Your Tests Clean and Isolated with a Context

Uri Bechar
Wix Engineering
4 min readJan 8, 2020

--

When writing tests we want them to describe the feature and the test case in a simple and clear way. Usually tests will need to have a setup prior to invoking and an expected result for the assertion. This can blow up our test code making the test long, not clear, full of details, and hard to read and maintain.

We also want to make the tests isolated, so we don’t have dependencies between the tests, and one test will not effect the outcome of another test.

Lets look at the following test. This test, tests that the UserService findUsersToDelete method, which gets users from an active directory service and from the database and returns users which are in the database and not in the active directory. As you can see in the test below we have all the setup and the expected result written in the test. This isolates the test but makes it hard to read and maintain.

@Test
public void should_returnUsersToDelete_that_areInDBAndNotInActiveDirectory(){
Address addressInDB = new Address.AddressBuilder()
.withCity("New York")
.withHouseNumber(3)
.withCountry("USA")
.withStreet("5th Ave")
.build();
User userFromDB = new User.UserBuilder()
.withAddress(addressInDB)
.withEmail("john@doe.com")
.withLastName("Doe")
.withFirstName("John")
.withId("SomeGuid1")
.build();
Address addressInActiveDirectory = new Address.AddressBuilder()
.withCity("New York")
.withHouseNumber(5)
.withCountry("USA")
.withStreet("5th Ave")
.build();
User userInActiveDirectory = new User.UserBuilder()
.withAddress(addressInActiveDirectory)
.withEmail("john@doe.com")
.withLastName("Doe")
.withFirstName("John")
.withId("SomeGuid1")
.build();
List<User> usersFromActiveDirectory = new ArrayList<User>();
usersFromActiveDirectory.add(userInActiveDirectory);
List<User> usersFromDB = new ArrayList<User>();
usersFromDB.add(userFromDB);
ActiveDirectoryService activeDirectoryService = mock(ActiveDirectoryService.class);
when(activeDirectoryService.getUsers())
.thenReturn(usersFromActiveDirectory);
UserDao userDao = mock(UserDao.class);
when(userDao.getUsers()).thenReturn(usersFromDB);
UserService userService = new UserService(activeDirectoryService,userDao);List<User> result = userService.findUsersToDelete();
List<User> expected = usersFromDB;
Assert.assertEquals(expected,result);
}

Lets refactor the test by extracting the setup and the expected result as members and methods of our test class:

public class UserServiceTest {
@Test
public void should_returnUsersToDelete_that_areInDBAndNotInActiveDirectory(){
UserService userService = new UserService(mockActiveDirectoryService(),mockUserDao());
List<User> result = userService.findUsersToDelete();
Assert.assertEquals(expected,result);
}
List<User> createUsersFromActiveDirectory(){
List<User> usersFromActiveDirectory = new ArrayList<User>();
usersFromActiveDirectory.add(userInActiveDirectory);
return usersFromActiveDirectory;
}
List<User> createUsersFromDB(){
List<User> usersFromDB = new ArrayList<User>();
usersFromDB.add(userFromDB);
return usersFromDB;
}
ActiveDirectoryService mockActiveDirectoryService(){
ActiveDirectoryService activeDirectoryService = mock(ActiveDirectoryService.class);
when(activeDirectoryService.getUsers())
.thenReturn(createUsersFromActiveDirectory());
return activeDirectoryService;
}
UserDao mockUserDao(){
UserDao userDao = mock(UserDao.class);
when(userDao.getUsers()).thenReturn(createUsersFromDB());
return userDao;
}
List<User> expected = createUsersFromDB();Address addressInDB = new Address.AddressBuilder()
.withCity(“New York”)
.withHouseNumber(3)
.withCountry(“USA”)
.withStreet(“5th Ave”)
.build();
User userFromDB = new User.UserBuilder()
.withAddress(addressInDB)
.withEmail(“john@doe.com”)
.withLastName(“Doe”)
.withFirstName(“John”)
.withId(“SomeGuid1”)
.build();
Address addressInActiveDirectory = new Address.AddressBuilder()
.withCity(“New York”)
.withHouseNumber(5)
.withCountry(“USA”)
.withStreet(“5th Ave”)
.build();
User userInActiveDirectory = new User.UserBuilder()
.withAddress(addressInActiveDirectory)
.withEmail(“john@doe.com”)
.withLastName(“Doe”)
.withFirstName(“John”)
.withId(“SomeGuid1”)
.build();
}

Now after we extracted the setup and the expected result, the test looks clean and readable. The setup and the expected are a “Context”, which the test is running in. Lets create an abstract class UserContext which will contain the setup and expected result. This will make our test class even cleaner.

public abstract class UserContext {
List<User> createUsersFromActiveDirectory(){
List<User> usersFromActiveDirectory = new ArrayList<User>();
usersFromActiveDirectory.add(userInActiveDirectory);
return usersFromActiveDirectory;
}
List<User> createUsersFromDB(){
List<User> usersFromADB = new ArrayList<User>();
usersFromADB.add(userFromDB);
return usersFromADB;
}
ActiveDirectoryService mockActiveDirectoryService(){
ActiveDirectoryService activeDirectoryService =
mock(ActiveDirectoryService.class);
when(activeDirectoryService.getUsers())
.thenReturn(createUsersFromActiveDirectory());
return activeDirectoryService;
}
UserDao mockUserDao(boolean emptyUserList){
UserDao userDao = mock(UserDao.class);
when(userDao.getUsers())
.thenReturn(createUsersFromDB());

return userDao;
}
List<User> expected = createUsersFromDB();Address addressInDB = new Address.AddressBuilder()
.withCity(“New York”)
.withHouseNumber(3)
.withCountry(“USA”)
.withStreet(“5th Ave”)
.build();
User userFromDB = new User.UserBuilder()
.withAddress(addressInDB)
.withEmail(“john@doe.com”)
.withLastName(“Doe”)
.withFirstName(“John”).withId(“SomeGuid1”)
.build();
Address addressInActiveDirectory = new Address.AddressBuilder()
.withCity(“New York”)
.withHouseNumber(5)
.withCountry(“USA”)
.withStreet(“5th Ave”)
.build();
User userInActiveDirectory = new User.UserBuilder()
.withAddress(addressInActiveDirectory)
.withEmail(“john@doe.com”)
.withLastName(“Doe”)
.withFirstName(“John”)
.withId(“SomeGuid1”)
.build();
}

and our test class will extend the UserContext

public class UserServiceTest extends UserContext {
@Test
public void should_returnUsersToDelete_that_areInDBAndNotInActiveDirectory() {
UserService userService =
new UserService(mockActiveDirectoryService(), mockUserDao());
List<User> result = userService.findUsersToDelete();
Assert.assertEquals(expected, result);
}
}

Our test class now has only the test logic ,which makes it easy to read and understand the feature and the test case we are testing.

Now lets add another test, in this test we want to test the option that the database returns an empty list of users.

public class UserServiceTest extends UserContext {
@Test
public void should_returnUsersToDelete_that_areInDBAndNotInActiveDirectory() {
UserService userService =
new UserService(mockActiveDirectoryService(), mockUserDao());
List<User> result = userService.findUsersToDelete();
Assert.assertEquals(expected, result);
}
@Test
public void should_returnAmEmptyUserList_when_DatabaseHasNoUsers() {
}
}

In our tests we are using a mocking framework (Mockito), as part of the setup we are configuring the mocks. Each test will need to have the mock configured differently. As we are using the same Context for all the tests in our test class, we need to make sure our Context class keeps the tests isolated.

In this case, we need to modify the mockUserDao method to return an empty list. We can do this by having the mock get its return result via its argument

UserDao mockUserDao(List<User> usersToReturn){
UserDao userDao = mock(UserDao.class);
when(userDao.getUsers())
.thenReturn(usersToReturn);
return userDao;
}

Now we have isolated the mock’s creation and each test can set it up as needed.

public class UserServiceTest extends UserContext {
@Test
public void should_returnUsersToDelete_that_areInDBAndNotInActiveDirectory() {
UserService userService = new UserService(mockActiveDirectoryService(),
mockUserDao(createUsersFromDB()));
List<User> result = userService.findUsersToDelete();
Assert.assertEquals(expected, result);
}
@Test
public void should_returnAmEmptyUserList_when_DatabaseHasNoUsers() {
UserService userService = new UserService(mockActiveDirectoryService(),
mockUserDao(new ArrayList<User>()));
List<User> result = userService.findUsersToDelete();
List<User> expectedEmptyList = new ArrayList<User>();
Assert.assertEquals(expectedEmptyList, result);
}
}

Summary

Having a generic Context class which contains methods for the tests setup and expected result, eliminates the boiler plate code from our test class, keeping it nice and clean. Having creation methods for our mocks in our context class, will keep our test isolated from one another.

photo by: Photo by JESHOOTS.COM on Unsplash

--

--