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

  1. 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) {
    	...
    }
    
}
  1. 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");
    }
}
  1. 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 {
...
}
  1. 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;
...
}
  1. (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));
    }