Java unit tests with Mockito

Java unit tests with Mockito

Purpose

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.

What is Mockito

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.

What makes you happy with a mock?

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

material

Installation

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.

Test target

Overview

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).

About notation

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.

Run the test

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.

Run the test

It is the same as the case of mock, and there is no particular explanation.

Various other uses

Change the value returned the first time and the second time

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 that the method was called

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.

Clear the number of method calls

@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 that the method was called multiple times

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 that the method has not been called

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).

Validate the arguments

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));
}

Verify the operation when an exception occurs

@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.

Other test related

I want to make the static method a mock

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.

I would like to test a private method

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

Java unit tests with Mockito
How to make Java unit tests (JUnit & Mockito & PowerMock)
Install java with Homebrew
Change seats with java
Install Java with Ansible
[Ralis] About unit tests
Comfortable download with JAVA
Java Unit Test Library-Artery-Sample
Switch java with direnv
Write tests with Minitest
Download Java with Ansible
Let's scrape with Java! !!
Unit test with Junit.
Build Java with Wercker
Endian conversion with JAVA
Run Android instrumentation unit tests with GitLab CI + Docker
Generate dummy data for various tests with Faker (java)
Easy BDD with (Java) Spectrum?
Use Lambda Layers with Java
Java multi-project creation with Gradle
Getting Started with Java Collection
Java Config with Spring MVC
Basic Authentication with Java 11 HttpClient
Let's experiment with Java inlining
Run batch with docker-compose with Java batch
[Template] MySQL connection with Java
Rewrite Java try-catch with Optional
Install Java 7 with Homebrew (cask)
[Java] JSON communication with jackson
Java to play with Function
Enable Java EE with NetBeans 9
[Java] JavaConfig with Static InnerClass
Let's operate Excel with Java! !!
Version control Java with SDKMAN
RSA encryption / decryption with java 8
Paging PDF with Java + PDFBox.jar
Various method tests with MockRestServiceServer
[Java] Content acquisition with HttpCliient
Java version control with jenv
Troubleshooting with Java Flight Recorder
Connect to DB with Java
Connect to MySQL 8 with Java
Error when playing with java
Getting Started with Java Basics
Seasonal display with Java switch
Use SpatiaLite with Java / JDBC
Compare Java 8 Optional with Swift
UnitTest with SpringBoot + JUnit + Mockito
Run Java VM with WebAssembly
Use Spring Test + Mockito + JUnit 4 for Spring Boot + Spring Retry unit tests
Screen transition with swing, java
[Java 8] Duplicate deletion (& duplicate check) with Stream
Java lambda expressions learned with Comparator
Validate arguments using ArgumentCaptor with mockito
Build a Java project with Gradle
Install java with Ubuntu 16.04 based Docker
Java to learn with ramen [Part 1]
Morphological analysis in Java with Kuromoji
Use java with MSYS and Cygwin
Distributed tracing with OpenCensus and Java
Java Unit Test Library-Artery-ArValidator Validates Objects