In this post, I will explain how to perform unit tests using Mockito and JUnit.
The sample is created by Spring Boot and uses constructor injection.
Therefore, annotations such as @ Mock
and @InjectMock
do not appear.
Mockito is an open source testing framework for Java released under the MIT license. (by Wikipedia) ~~ Read as Mock Ito. ~~ Read as Moquito. You can easily create a mock object for testing by using this.
The following tests are awkward to write.
--Branch the operation according to the value of DB → Do you rewrite the DB in the test? How do you prepare the DB for the test? Is it initialized with the same value every time? --Intentionally generate an error → How do you do the test when the disk full occurs? --Date test → Do you want to change the system time?
You can solve these problems by replacing the class you want to test with a class (mock) that behaves in the same way as the dependent class. The advantages are as follows.
--The test target is limited and it is easy to test --Since the DB is not actually accessed, the test execution is completed at explosive speed. --You can intentionally raise an exception --Can be tested even if the dependent class implementation is not complete
plugins {
id 'org.springframework.boot' version '2.3.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.mockito'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
test {
useJUnitPlatform()
}
Mockito is included in the Spring Boot sterter test, so there is no need to add it from the default of Spring Boot. I added it because I wanted to use only Lombok.
Take the POS system that calculates the total price of products as an example. The total amount is calculated by adding the tax rate set for each product type. The tax rate is subject to change in the future, so we will obtain it from the DB.
Product object definition
package com.example.mockito;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
*Product classification
*
* @author rcftdbeu
*
*/
enum ItemType {
Food, Other
}
/**
*Product
*
* @author rcftdbeu
*
*/
@Getter
@Setter
@AllArgsConstructor
public class Item {
/**name*/
String name;
/**price*/
int price;
/**type*/
ItemType itemType;
}
Service to calculate the total amount
package com.example.mockito;
import java.util.List;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class POS {
private final TaxRateMaster taxRateMaster;
/**
*Calculate the total price of the item
*
* @param itemList
* @return
*/
public int culculateTotal(List<Item> itemList) {
//total fee
int sum = 0;
//Tax rate is calculated and added one by one
for (Item item : itemList) {
sum += item.getPrice() * (1 + taxRateMaster.getTaxRate(item.getItemType()));
}
return sum;
}
}
Class that contains the DB access to be mocked
package com.example.mockito;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
/**
*Get the tax rate
*
* @author rcftdbeu
*
*/
@RequiredArgsConstructor
@Component
public class TaxRateMaster {
private final TaxRepository ripository;
/**
*Today's date
*
* @return
*/
public Date getSysDate() {
return new Date();
}
/**
*Is it after the reduced tax rate application date?
*
* @return
*/
private boolean isAfterApplyDate() {
String strDate = "2019/10/1 00:00:00";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
try {
Date applyDate = dateFormat.parse(strDate);
return getSysDate().after(applyDate);
} catch (Exception e) {
}
return false;
}
/**
*Get the tax rate
*
* @param type
* @return
*/
public Double getTaxRate(ItemType type) {
//After applying the reduced tax rate
if (isAfterApplyDate()) {
if (type == ItemType.Food) {
return ripository.getFoodTaxRate();
} else {
return ripository.getOtherTaxRate();
}
}
//Before applying the reduced tax rate
return ripository.getOldTaxRate();
}
}
Repository
package com.example.mockito;
import org.springframework.stereotype.Repository;
/**
*Processing to get tax rate from DB<br>
*Not implemented because DB is not used<br>
*Exists only to avoid compilation errors
*
* @author rcftdbeu
*
*/
@Repository
public class TaxRepository {
public Double getFoodTaxRate() {
return null;
}
public Double getOtherTaxRate() {
return null;
}
public Double getOldTaxRate() {
return null;
}
}
Unit Test
Finally the main subject.
There are two types available in Mockito: mock
and spy
.
mock: A mock in which all methods of the target class are replaced with return null
. No initialization required.
spy: Need to be initialized with the same object as the target class. Only specific methods can be changed as needed.
Mock
We will carry out tests using mock.
package com.example.mockito;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
public class MockTest {
private static POS pos;
private static TaxRateMaster master;
private static TaxRepository repository;
@Nested
Testing with static class Mock{
//Run only once before testing
@BeforeAll
static void init() {
master = mock(TaxRateMaster.class);
pos = new POS(master);
}
//Run before each test
@BeforeEach
void setup() {
//Food tax rate is 8%
when(master.getTaxRate(ItemType.Food)).thenReturn(0.08);
//Other tax rates are 10%
when(master.getTaxRate(ItemType.Other)).thenReturn(0.10);
}
@Test
void Food reduced tax rate calculation_Calculated at 8%() {
//Add 100 yen rice balls
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("rice ball", 100, ItemType.Food);
itemList.add(onigiri);
//Calculation
int sum = pos.culculateTotal(itemList);
//Check the result
assertThat(sum, is(108));
}
@Test
void Other reduced tax rate calculation_Calculated at 10%() {
//Added 500 yen magazine
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("magazine", 500, ItemType.Other);
itemList.add(onigiri);
//Calculation
int sum = pos.culculateTotal(itemList);
//Check the result
assertThat(sum, is(550));
}
}
}
init()
The mock is being initialized. The TaxRateMaster on which POS depends is replaced with a mock. The structure is as follows. POS entity-TaxRateMaster mock
setup()
It defines the behavior of the mock.
when ()
defines which method is called and thenReturn ()
returns what.
If you want to define the behavior when executed with a specific argument, specify that value directly. To define the behavior when executed with arbitrary arguments, specify anyString () or any (ItemType.class).
There are two methods, one is to write with when (). thenReturn ()
and the other is to write with doReturn (). When ()
.
If you rewrite the setup example, it will be doReturn (0.08) .when (master) .getTaxRate (ItemType.Food);
.
Which is better is controversial.
when (). thenReturn () faction --⭕️ Easy to read in the same order of if statements -❌ May not be available with spy --❌ thenThrow is not available for methods that return void → If you design correctly, you will not have the opportunity to use doReturn.
doReturn (). When () faction --⭕️ You can use it anytime without worrying about it -❌ The value described in doReturn () becomes Object type and the compiler cannot detect the error. → It's easier to unify with doReturn than to remember subtle differences, and you can find out by executing type check.
Personally, I like when (). ThenReturn (), which is easy to read, so I use this notation in this post as well.
There is nothing special, just create an Item instance for testing and execute culculateTotal to be tested.
Spy
@Nested
Testing with static class Spy{
//Run only once before testing
@BeforeAll
static void init() {
repository = mock(TaxRepository.class);
master = spy(new TaxRateMaster(repository));
pos = new POS(master);
}
//Run before each test
@BeforeEach
void setup() {
//The tax rate before applying the reduced tax rate is 8%
when(repository.getOldTaxRate()).thenReturn(0.08);
//Food tax rate is 8%
when(repository.getFoodTaxRate()).thenReturn(0.08);
//Other tax rates are 10%
when(repository.getOtherTaxRate()).thenReturn(0.10);
}
@Nested
class Before applying the reduced tax rate{
@BeforeEach
void setup() throws ParseException {
//Set to return the date before the reduced tax rate is applied when the current date is acquired
String strDate = "2019/9/30";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
Date applyDate = dateFormat.parse(strDate);
when(master.getSysDate()).thenReturn(applyDate);
}
@Test
void Food reduced tax rate calculation_Calculated at 8%() {
//Add 100 yen rice balls
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("rice ball", 100, ItemType.Food);
itemList.add(onigiri);
//Calculation
int sum = pos.culculateTotal(itemList);
//Check the result
assertThat(sum, is(108));
}
@Test
void Other reduced tax rate calculation_Calculated at 8%() {
//Added 500 yen magazine
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("magazine", 500, ItemType.Other);
itemList.add(onigiri);
//Calculation
int sum = pos.culculateTotal(itemList);
//Check the result
assertThat(sum, is(540));
}
}
@Nested
class After applying the reduced tax rate{
@BeforeEach
void setup() throws ParseException {
//Set to return the date before the reduced tax rate is applied when the current date is acquired
String strDate = "2019/10/1 00:00:01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date applyDate = dateFormat.parse(strDate);
when(master.getSysDate()).thenReturn(applyDate);
}
@Test
void Food reduced tax rate calculation_Calculated at 8%() {
//Add 100 yen rice balls
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("rice ball", 100, ItemType.Food);
itemList.add(onigiri);
//Calculation
int sum = pos.culculateTotal(itemList);
//Check the result
assertThat(sum, is(108));
}
@Test
void Other reduced tax rate calculation_Calculated at 10%() {
//Added 500 yen magazine
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("magazine", 500, ItemType.Other);
itemList.add(onigiri);
//Calculation
int sum = pos.culculateTotal(itemList);
//Check the result
assertThat(sum, is(550));
}
}
}
init()
Initializing Spy. Set TaxRateMaster on which POS depends to Spy and set it to Spy. The structure is as follows. POS entity-TaxRateMaster's Spy-TaxRepository mock
setup()
Only the part to get the current date in Spy of TaxRateMaster is replaced with the defined date.
It is the same as the case of mock, and there is no particular explanation.
when(master.getTaxRate(ItemType.Food)).thenReturn(0.08)
.thenReturn(0.10);
By writing by connecting thenReturn
, different values can be returned in the first and second calls.
The first time can be used for registration, and the second time can be used for processing such as updating.
verify(master).getTaxRate(ItemType.Food);
You can verify that the mock method was called by using verify
.
You can also verify that the argument at the time of method execution is executed with the specified argument.
@BeforeEach
void setup() {
clearInvocations(master);
}
There is only one instance of the mock, and if it is executed from multiple tests, the number of calls in other tests will be counted when verifying with verify
, so before executing the test, use clearInvocations
to count the number of mock calls. You need to reset it.
verify(master, times(2)).getTaxRate(ItemType.Food);
You can verify that the method has been called the specified number of times by adding times
to the argument of verify
.
verify(master, never()).getTaxRate(ItemType.Food);
You can verify that the method is not called by adding never
to the argument of verify
.
The same is true for times (0)
.
If the argument of the method you want to verify is a primitive type such as String or Boolean, you could verify it with the argument of the method after verify, but for other objects, You can get the arguments by using ArgumentCaptor.
@Test
Get void argument() {
//Generate ArgumentCaptor to get arguments
ArgumentCaptor<ItemType> argCaptor = ArgumentCaptor.forClass(ItemType.class);
//Add 100 yen rice balls
List<Item> itemList = new ArrayList<Item>();
itemList.add(new Item("rice ball", 100, ItemType.Food));
//Calculation execution
pos.culculateTotal(itemList);
//Get arguments
verify(master).getTaxRate(argCaptor.capture());
ItemType executedItemType = argCaptor.getValue();
//Validate run-time arguments
assertThat(executedItemType, is(ItemType.Food));
}
@Test
void Exception occurred() {
when(master.getTaxRate(ItemType.Food)).thenThrow(new RuntimeException("DB connection failure"));
//Add 100 yen rice balls
List<Item> itemList = new ArrayList<Item>();
itemList.add(new Item("rice ball", 100, ItemType.Food));
//Calculation
assertThrows(RuntimeException.class, () -> pos.culculateTotal(itemList));
//then clear Throw settings
reset(master);
}
You can raise an exception by setting thenThrow
instead of thenReturn
.
Exceptions can be set only for exceptions thrown by the corresponding method. Since no exception is set in this method, RuntimeException
is thrown.
Also, I don't want to throw an exception in other tests, so I cleared it with reset
.
By default, it cannot be mocked. The static method is incompatible with mock, so please consider how to make it non-static if possible. It seems that it can be converted to mock by using Power mock, but I didn't know how to move it with JUnit5.
This also cannot be mocked. The private method should be able to be tested through the public method, so let's implement the public method test. If you really want to, you'll have to use a Power mock or start black magic such as reflection.
Recommended Posts