JUnit and Mockito Unit Testing Tips - Nakanz/maunakea GitHub Wiki
Why Use JUnit Tests with Mockito Mock Data
Benefits of using Mock data
JUnit can execute unit tests on code that utilizes DAOs without mocking them and possibly output the correct result but...
- The unit test can be slowed down by having to connect to a database to retrieve data from the DAO call
- The unit test has to rely on an external source (a database), may not be valid later due to changes made in the database
- Testing a simple method becomes tedious and makes more sense to skip testing
- Data in the database could be altered by a unit test
That's where Mockito can help us.
The implementation of both JUnit and Mockito allows Unit Tests to be applied to code that is independent of database calls. Unit Tests should be fast and isolated from the rest of the project, only verifying that the developer's code is performing correctly. By mocking DAO calls and testing an expected interaction, Unit Tests become very efficient and easy to create.
- Good JUnit Tests ensures code is working properly in every situation. Thorough tests help developers pinpoint and fix areas where the code can fail.
- Mockito eliminates the issue of dealing with database changes. It also increases execution speed by simulating behaviors for specified DAOs.
- JUnit and Mockito provide swift unit tests by assuming Spring's framework is correct so it can focus solely on the functionality of the developer's code.
Implementation
JUnit and Mockito are both included in Spring's test implementation. Add this to your build.gradle dependencies
dependencies {
testImplementation ('org.springframework.boot:spring-boot-starter-test')
}
JUnit Refresher
To Create a JUnit Test Case in Eclipse:
Right-click inside Package Explorer -> New -> JUnit Test Case
Then Select JUnit 4
Here's a SimpleCalculator class that needs to be tested
public class SimpleCalculator {
public int x;
public int y;
private int result;
public SimpleCalculator() {
x = 0;
y = 0;
result = 0;
}
/* calculator functions */
public void clear() {
x = 0;
y = 0;
result = 0;
}
public int add(int x, int y) {
result = x + y;
return result;
}
public int subtract(int x, int y) {
result = x - y;
return result;
}
public int multiply(int x, int y) {
result = x * y;
return result;
}
public int divide(int x, int y) {
result = x/y;
return result;
}
public boolean isClear() {
if (x == 0 && y == 0 && result == 0) {
return true;
}
return false;
}
public int getAnswer() {
return result;
}
}
JUnit Assert Syntax
The following methods from the Assert package will be used to create our various unit tests.
import static org.junit.Assert.*;
assertEquals(expected, actual)
- Determines if second parameter (actual) is equal to the expected parameter
assertTrue(boolean statement)
- Assert that this boolean statement will be true
assertFalse(boolean statement)
- Assert that this boolean statement will be false
assertNull(Object)
- Assert that this Object is null
assertNotNull(Object)
- Assert that this Object is not null
Testing SimpleCalculator
Hereʻs an example of how to create unit tests to verify SimpleCalculatorʻs methods behave correctly. The next section will go over Mockito, and itʻs functionality; then the combination of the two to simulate DAO calls and test dev code.
package scratch;
import static org.junit.Assert.*;
import org.junit.Test;
public class SimpleCalculatorTest {
@Test
public void testAdd() {
// create SimpleCalculator object to begin testing
SimpleCalculator s = new SimpleCalculator();
// test the add function, s.getAnswer should return 15
s.add(5, 10);
assertEquals(15, s.getAnswer());
s.clear();
// test if s was cleared with the boolean .isClear() function, assert True
assertTrue(s.isClear());
}
@Test
public void testSubtract() {
SimpleCalculator s = new SimpleCalculator();
s.subtract(10, 5);
// assert that s' result is not null
assertNotNull(s.getAnswer());
// check if subtract correctly subtracted 5 from 10, answer should be 5
assertEquals(5, s.getAnswer());
s.clear();
// check if s was cleared
assertTrue(s.isClear());
}
@Test
public void testMultiply() {
SimpleCalculator s = new SimpleCalculator();
// test the multiply method, assert that its result is not null
s.multiply(5, 5);
assertNotNull(s.getAnswer());
// assert that the correct value is returned
assertEquals(25, s.getAnswer());
// assert that s was cleared
s.clear();
assertTrue(s.isClear());
}
@Test
public void testDivide() {
SimpleCalculator s = new SimpleCalculator();
// test divide
s.divide(20, 2);
// assert that the answer is not null
assertNotNull(s.getAnswer());
// assert the correct result was returned
assertEquals(10, s.getAnswer());
// assert that s was cleared
s.clear();
assertTrue(s.isClear());
}
}
How To Use Mockito
These are the main functions provided by Mockito to utilize mocked objects.
when(T methodCall)
import static org.mockito.Mockito.when;
The parameter T methodCall is a method within your mocked object. Use it when you want the mocked object to return a particular value when a particular method is called
when(mockedDAO.someMethod("some argument")).thenReturn("something");
returns: something
when(mockedDAO.someMethod("some argument")).thenThrow(new RuntimeException());
returns: RuntimeException
- tells Mockito what to do when a particular mocked method is executed
verify(T mock) & verify(T mock, VerificationMode mode)
import static org.mockito.Mockito.verify;
verify(T mock) can be used to verify that a certain behavior for a mocked object has happened one time. Basically verify acts like an assert statement for mocked objects.
verify(mockedDAO).someMethod("some argument");
if mockedDAO.someMethod("some argument") was only called once then verify will return true
verify(T mock, VerificationMode mode) expands on the previous verify method by allowing for special scenarios and for specifying occurrences.
verify(mockedDAO, never()).someMethod("some argument");
fails if the method IS called
verify(mockedDAO, times(3)).someMethod("some argument");
fails if method is not called 3 times
any(Class type)
import static org.mockito.ArgumentMatchers.any;
Sometimes there is no need to have a specified parameter, and can be simpler to say "any String" or "any UserAccountEntity".
verify that mockedDAO.someMethod() returned some type of data once
verify(mockedDAO).someMethod(any(DataType.class));
when mockedDAO.someMethod() with any valid parameter, return something
when(mockedDAO.someMethod(any(DataType.class)).thenReturn("something");
Set Up Mockito in JUnit
- Determine what DAOs are used by the class being tested
The following UserServiceImpl class relies on two DAO, PasswordRepository and UserRepository, which will be mocked with Mockito in JUnit.
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordRepository passwordRepository;
@Override
public User loginUser(LoginVO creds) {
...
}
}
- Create a JUnit Test Case in Eclipse
Right-click inside Package Explorer -> New -> JUnit Test Case
Then Select JUnit 4
The JUnit annotation @Test
tells JUnit that this method is a Test.
public class TestUserServiceImplLoginUserMethod {
@Test
public void test() {
fail("Not yet implemented");
}
}
- Set up JUnit class to run its tests using
MockitoJUnitRunner.class
with the annotation@RunWith()
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TestUserServiceImplLoginUserMethod {
...
}
- Mock DAO's with the annotation @Mock and initialize the class being tested with @InjectMocks
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TestUserServiceImplLoginUserMethod {
@InjectMocks
private UserServiceImpl userServiceImpl;
@Mock
private UserRepository userRepository;
@Mock
private PasswordRepository passwordRepository;
...
}
- (optional) Set up variables in a method with the annotation @Before
When testing, JUnit's @Before
annotation helps reduce redundant code by performing something before the tests are executed, such as setting up instance variables.
Below validUser
and invalidUser
are being initialized in setUp()
It's not always necessary to use this annotation, but it can be very useful.
@RunWith(MockitoJUnitRunner.class)
public class TestUserServiceImplLoginUserMethod {
@InjectMocks
private UserServiceImpl userServiceImpl;
@Mock
private UserRepository userRepository;
@Mock
private PasswordRepository passwordRepository;
private LoginVO creds;
private UserAccountEntity validUser;
private UserAccountEntity invalidUser;
private final String ENCRYPTED_PASSWORD = "848422F70E";
private final String RAW_PASSWORD = "pas$$worD";
@Before
public void setUp() {
validUser = GenerateUser.buildValidUser();
invalidUser = GenerateUser.buildInvalidUser();
}
...
}
Writing a Unit Test for UserServiceImpl's loginUser()
Goal: Test specific situations to ensure loginUser() behaves correctly using the DAOs UserRepository
and PasswordRepository
userRepository & passwordRepository methods
userRepository.getUserAccountEntityByUsername(attemptedUsername)
- returns an actual User Account Entity to compare to an attempted username
passwordRepository.findFirstByUserAccountIdOrderByEffectiveDateDesc(userAccountId)
- returns an actual Password Entity to compare to an attempted password
Shown below is the loginUser() method from UserServiceImpl.
If attempted password and/or username are incorrect, an IllegalArgumentException
is thrown. Otherwise loginUser() returns the specified User Object from the UserRepository call.
Notice where the DAO calls occur.
@Service
public class UserServiceImpl implements UserService {
...
@Override
public User loginUser(LoginVO creds) {
String attemptedUsername = creds.getUsername();
String attemptedCurrentPassword = creds.getPassword();
//DAO call
UserAccountEntity userAccountEntity = userRepository.getUserAccountEntityByUsername(attemptedUsername);
if (userAccountEntity != null) {
//DAO call
PasswordEntity pEntity = passwordRepository.findFirstByUserAccountIdOrderByEffectiveDateDesc(userAccountEntity.getUserAccountId());
String currentPassword = pEntity.getPassword();
PasswordEncryption passwordEncryption = new PasswordEncryption();
if (currentPassword != null && passwordEncryption.authenticate(attemptedCurrentPassword, currentPassword)) {
//password was correct
User userData = new User(userAccountEntity.getFirstName(), userAccountEntity.getLastName(),
userAccountEntity.getEmail(), userAccountEntity.getUsername(),
userAccountEntity.getUserAccountId());
return userData;
//password was incorrect
}
}
//username OR password was incorrect
throw new IllegalArgumentException("invalid password or username");
}
...
}
Test loginUser() with an invalid password, and valid username
Expected:
userRepository.getUserAccountEntityByUsername(username)
should return a valid UserAccountEntity using the attemptedUsername
passwordRepository.findFirstByUserAccountIdOrderByEffectiveDateDesc(userAccountId)
should return a valid PasswordEntity for the currentPassword to compare with attemptedCurrentPassword
if (currentPassword != null && passwordEncryption.authenticate(attemptedCurrentPassword, currentPassword)){}
should fail because attemptedCurrentPassword is invalid
- should not return a User object
- should throw an exception
@RunWith(MockitoJUnitRunner.class)
public class TestUserServiceImplLoginUserMethod {
...
@Test
public void incorrectPasswordCorrectUsernameFailedLogin() {
/*SET UP TEST*/
// valid username ---- invalid password
creds = new LoginVO(validUser.getUsername(), "invalidPassword");
// set userRepository's method to take in any String and to return validUser
when(userRepository.getUserAccountEntityByUsername(any(String.class))).thenReturn(validUser);
// create a valid Password Entity
PasswordEntity passwordEntity = new PasswordEntity();
passwordEntity.setPassword(ENCRYPTED_PASSWORD);
passwordEntity.setUserAccountId(4);
// Use created Password Entity to be the returned object of mocked PasswordRepository
when(passwordRepository.
findFirstByUserAccountIdOrderByEffectiveDateDesc(any(Integer.class)))
.thenReturn(passwordEntity);
/*SET UP FINISHED*/
/* TEST loginUser()*/
try {
userServiceImpl.loginUser(creds);
} catch(IllegalArgumentException e) {
// ensure the correct exception was thrown
assertEquals("invalid password or username", e.getMessage());
// verify DAO were called at least once
verify(userRepository).getUserAccountEntityByUsername(any(String.class));
verify(passwordRepository).
findFirstByUserAccountIdOrderByEffectiveDateDesc(any(Integer.class));
// in the unedited version, userRepository saves new data if credentials are valid
// since they are not in this case, .save should NEVER BE CALLED
verify(userRepository, never()).save(any(UserAccountEntity.class));
}
}
...
}
This test will pass if assertEquals inside the catch verifies the correct exception is thrown by loginUser(). We want to verify the correct behavior when a client enters the correct username and an INVALID password.
Verify statements ensure that the correct DAO were called. userRepository and passwordRepository were both called, and since the password was invalid, userRepository never saved any data; which is what we want for this particular test.
More UserServiceImpl Tests
Client Enters the Correct Credentials and Successfully Logins in
- Both mocked DAO should be called and return valid entities
- loginUser() should return a User object that is NOT null
- loginUser() should not throw an exception
- A UserAccountEntity should be saved to the userRepository
@Test
public void validCredentialsSuccessfulLogin() {
/* SET UP */
creds = new LoginVO(validUser.getUsername(), RAW_PASSWORD);
when(userRepository.getUserAccountEntityByUsername(any(String.class))).thenReturn(validUser);
PasswordEntity passwordEntity = new PasswordEntity();
passwordEntity.setPassword(encryptedPassword);
passwordEntity.setUserAccountId(4);
when(passwordRepository.findFirstByUserAccountIdOrderByEffectiveDateDesc(any(Integer.class))).thenReturn(passwordEntity);
/* SET UP FINISHED */
/* CALL METHOD */
User returnUser = userServiceImpl.loginUser(creds);
// if returnUser is null then login has failed
assertNotNull(returnUser);
// ensure DAO were called
verify(userRepository).getUserAccountEntityByUsername(any(String.class));
verify(passwordRepository).findFirstByUserAccountIdOrderByEffectiveDateDesc(any(Integer.class));
verify(userRepository).save(any(UserAccountEntity));
// check if loginUser(creds) returned the correct user
assertEquals(4, returnUser.getUserAccountId());
}
Client Enters a Username that Doesn't Exist
- PasswordRepository should NEVER be called because username is INVALID
- UserRepository should be called and return null because username does not exist
// @Test parameter expects an exception to be thrown. If no exception is thrown this test fails
@Test(expected = IllegalArgumentException.class)
public void loginInvalidUserName() {
/* SET UP */
creds = new LoginVO(invalidUser.getUsername(), ENCRYPTED_PASSWORD);
// username does not exist, will return null...set up DAO return to null
when(userRepository.getUserAccountEntityByUsername(any())).thenReturn(null);
/* SET UP FINISHED */
/* CALL METHOD */
userServiceImpl.loginUser(creds);
// verify the userRepo was called
verify(userRepository).getUserAccountEntityByUsername(any(String.class));
// verify that the passwordRepo was NOT called
verify(passwordRepository, never()).findFirstByUserAccountIdOrderByEffectiveDateDesc(any(Integer.class));
}