[JAVA] Test code using mock with JUnit (EasyMock center)

About testing with mock in JUnit. This is the second article to disseminate test code in the workplace. It's not a very recent story, as it focuses on EasyMock.

Execution environment

What is a mock

A Mock Object is a type of lower module stub that is a substitute for software testing, especially in test-driven development and behavior-driven development. Module to be inspected compared to stub Is used to verify that it is using its submodules correctly.

Mock object Source: Free encyclopedia "Wikipedia"

To use it, for example, suppose you have created a class called HelloJunit as shown below. When trying to test this class, if the GreetDao class is not created (and the DB referenced by Dao is not prepared), if you test it normally, an error will occur. I think it will end up.

HelloJunit


public class HelloJunit {

	@Binding
	GreetDao greetDao;

	public String sayGreeting() {
		return greetDao.getGreet();
	}

Originally, I just want to test the HelloJunit class, so I should be able to test it even if the code of the dependency (GreetDao) is not made, so let's prepare a Haribote class so that it can work for the time being. That is the role of the mock.

How to make a mock

There are the following methods.

Mock libraries include Mockito, JMockit, and ʻEasyMock, MockInterceptor`.

I have the impression that Mockito is often used these days.

In Seasar2, ʻEasyMock and MockInterceptor` are included by default. I think it is difficult to install a new library in an environment where Seasar2 is still used, so I would like to proceed with EasyMock this time.

Test code sample using EasyMock

Code for the time being

The product code has been slightly changed from the contents introduced above.

Product code


public class HelloJunit {

	GreetDao greetDao;
	
	public HelloJunit(GreetDao greetDao) {
		this.greetDao = greetDao;
	}

	public String sayGreeting() {
		return greetDao.getGreet();
	}
}

Here is the test code.

Test code


import static org.easymock.EasyMock.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.seasar.framework.unit.annotation.EasyMock;
import org.seasar.sastruts.example.dao.GreetDao;

public class HelloJunitTest {

	HelloJunit helloJunit;

	@EasyMock
	GreetDao greetDao;

	@Before
	public void setUp() {
		greetDao = createMock(GreetDao.class);
		helloJunit = new HelloJunit(greetDao);
	}

	@Test
public void say hello() throws Exception {
		//setup
		expect(greetDao.getGreet()).andReturn("Hello");
		replay(greetDao);

		//exercise
		String actual = helloJunit.sayGreeting();

		//verify
		String expected = "Hello";
		assertThat(actual, is(expected));
		verify(greetDao);

		//tear down (post-processing)
		//if you have some trouble···
	}
}

I think you can understand it somehow, but I will explain it.

EasyMock annotation

I have GreetDao as a member variable, but it has ʻEasyMock annotation`. By attaching this, you can use EasyMock.

createMock The setUp method uses the createMock method to create a greetDao. By passing the greetDao created here when creating an instance of helloJunit, which is the method to be tested, the mock will be set.

Setup in test method

In the comment setup, we use the ʻexpect` method. This is the most important part that determines the behavior of the mock.

When greetDao.getGreet () is called like this, it is set to return ~. Specify how to call the method with ʻexpected (). Then set the return value with ʻandReturn (). In this case, when greetDao.getGreet () with no argument is called, "Hello" is returned.

The method called replay is an image that remembers the behavior of the mock set immediately before. Make sure to replay after executing expected ().

exercise Here, the method you want to test is executed and stored in a variable. It is said that it is easy to understand if the variables to be stored are unified to ʻactual` regardless of the type.

verify Here we are comparing the return and expected values of helloJunit.sayGreeting ();. Also, verify (greetDao); verifies that the behavior of the mock specified in setup was called as expected. If helloJunit.sayGreeting (); has never been called, an error will be returned.

tear down This is the post-test post-processing. For example, if test data has been input to the DB, that data will be deleted.

How to use EasyMock

Check arguments

This time, check if the findDreet method of greetDao is called as expected.

Code to be tested


public String sayGreeting() {
	return greetDao.findGreet(1);
}

In ʻexpect (), write the expected method call method as it is. In this case, it is assumed that 1 is included in the argument of findGreet, so just describe findGreet (1) `.

Test code


@Test
public void argument test() throws Exception {
	//setup
	expect(greetDao.findGreet(1)).andReturn("Hello");
	replay(greetDao);

	//exercise
	String actual = helloJunit.sayGreeting();

	//verify
	verify(greetDao);
}

By the way, if it is called by findGreet (2);, the following error will be output.

Reference in case of error


java.lang.AssertionError: 
  Unexpected method call findGreet(2):
    findGreet(1): expected: 1, actual: 0
	at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:32)
	at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:61)
	at com.sun.proxy.$Proxy4.findGreet(Unknown Source)
	at org.seasar.sastruts.example.service.HelloJunit.sayGreeting(HelloJunit.java:17)
	at org.seasar.sastruts.example.service.HelloJunitTest.Argument test(HelloJunitTest.java:52)
--Omitted thereafter--

Set the return value and test the subsequent processing

I tried to insert a branch by the return value of findGreet. In this test, I would like to test the pattern in which pasta is returned.

Production code


public String sayGreeting() {
	String greet = greetDao.findGreet(1);
	if (greet.contentEquals("Hello")) {
		return "pasta";
	} else {
		return "ramen";
	}
}

To set the return value, specify the return value in ʻandReturn`. In this case, "Hello" will be returned.

Test code


@Test
public void argument test() throws Exception {
	//setup
	expect(greetDao.findGreet(1)).andReturn("Hello");
	replay(greetDao);

	//exercise
	String actual = helloJunit.sayGreeting();

	//verify
	String expected = "pasta";
	assertThat(actual, is(expected));
	verify(greetDao);
}

Test Void type methods

How to test a void type method.

Production code (GreetDao)


public interface GreetDao {
	public void insertGreet(String greet);
}

Production code (HelloJunit)


public String addGreet() {
	greetDao.insertGreet("Good Night");
	return "OK";
}

If you want to test the void type, actually execute the void method in your test code. Then call ʻexpectLastCall ()`. This allows you to remember and validate the previous method execution.

By the way, when the same void method is executed twice, it is verified whether it has been executed the number of times by adding times () such as ʻexpectLastCall (). Time (2);`.

Test code


@Test
test public void void method() throws Exception {
	//setup
	greetDao.insertGreet("Good Night");
	expectLastCall();
	replay(greetDao);

	//exercise
	String actual = helloJunit.sayGreeting();

	//verify
	verify(greetDao);
}

Easy Mock notes

EasyMock can only mock interface classes. If you want to mock a concrete class, try the inheritance method below.

Inherit and check

If the class used in the method under test is not an interface, or if it is inconvenient to test, you may inherit it for testing and prepare your own mock class.

For example, if the beans returned by the method do not override the equals method and it is awkward to check each field, inherit equals and test as follows.

Production code to be tested


public TransferResultDto transfer(String payerAccountId,
										String payeeAccountId,
										String payerName,
										long transferAmount) {

		Balance payerBalance = balanceDao.findByAccountId(payerAccountId);
		if (payerBalance == null)
			throw new BusinessLogicException("I can't get my balance");

		if (transferAmount > payerBalance.amount)
			throw new BusinessLogicException("Not enough balance");

		Balance payeeBalance = balanceDao.findByAccountId(payeeAccountId);
		if (payeeBalance == null)
			throw new BusinessLogicException("Transfer account does not exist");

		LocalDateTime transferDate = LocalDateTime.now();

		Transfer peyerTransaction = new Transfer();
		peyerTransaction.accountId = payerAccountId;
		peyerTransaction.name = payerBalance.name;
		peyerTransaction.transferAmount = -transferAmount;
		peyerTransaction.transferDate = transferDate;

		Transfer payeeTransaction = new Transfer();
		payeeTransaction.accountId = payeeAccountId;
		payeeTransaction.name = payeeBalance.name;
		payeeTransaction.transferAmount = transferAmount;
		payeeTransaction.transferDate = transferDate;

		transferDao.insertTransfer(peyerTransaction);
		transferDao.insertTransfer(payeeTransaction);
		balanceDao.updateAmount(payerAccountId, -transferAmount);
		balanceDao.updateAmount(payeeAccountId, transferAmount);

		Balance updatedPayerBalance = balanceDao.findByAccountId(payerAccountId);

		return new TransferResultDto(payerAccountId, payeeAccountId,
									 payerBalance.name, payeeBalance.name,
									 transferAmount,
									 updatedPayerBalance.amount);
	}
}

Return bean


public class TransferResultDto {

	private final String payerAccountId;
	private final String payeeAccountId;
	private final String payerName;
	private final String payeeName;
	private final long transferAmount;
	private final long amount;

	public TransferResultDto(String payerAccountId,
							 String payeeAccountId,
							 String payerName,
							 String payeeName,
							 long transferAmount,
							 long amount) {

		this.payerAccountId = payerAccountId;
		this.payeeAccountId = payeeAccountId;
		this.payerName = payerName;
		this.payeeName = payeeName;
		this.transferAmount = transferAmount;
		this.amount = amount;
	}

	public String getPayerAccountId() {
		return payerAccountId;
	}

	public String getPayeeAccountId() {
		return payeeAccountId;
	}

	public String getPayerName() {
		return payerName;
	}

	public String getPayeeName() {
		return payeeName;
	}

	public long getTransferAmount() {
		return transferAmount;
	}

	public long getAmount() {
		return amount;
	}

}

If you want to test this, create a TransferResultDtoMock class that inherits from TransferResultDto in the test class. In this TransferResultDtoMock class, prepare an equals method that compares all fields, and when testing, you can compare with is like ʻassertThat (actual, is (expected));`.

python


public class TransferServiceTest {
	@Test
public void try inheritance mock() {
		// setup
		TransferResultDtoMock expected = 
						new TransferResultDtoMock("1", "2",
												ACCOUNT1_BEFORE_BALANCE.name, ACCOUNT2_BALANCE.name,
												2000, ACCOUNT1_AFTER_BALANCE.amount);

		TransferResultDto transferResultDto = transferService.transfer("1", "2", "Taro Tanaka", 1000);
		TransferResultDtoMock actual = new TransferResultDtoMock(transferResultDto);
		assertThat(actual, is(expected));
	}


	@EqualsAndHashCode(callSuper = true)
	private class TransferResultDtoMock extends TransferResultDto {
		public TransferResultDtoMock(String payerAccountId,
									 String payeeAccountId,
									 String payerName,
									 String payeeName,
									 long transferAmount,
									 long amount) {

			super(payerAccountId, payeeAccountId, payerName,
					payeeName, transferAmount, amount);

		}

		public TransferResultDtoMock(TransferResultDto transferResultDto) {
			super(transferResultDto.getPayerAccountId(),
				  transferResultDto.getPayeeAccountId(),
				  transferResultDto.getPayerName(),
				  transferResultDto.getPayeeName(),
				  transferResultDto.getTransferAmount(),
				  transferResultDto.getAmount());
		}
				

	}
}

Things to be aware of when writing test code

Make the part related to business logic an independent implementation

First, I think that the logic to DI GreetDao in HelloJunit was changed from annotation to constructor type. The reason is that when expressing with annotations, HelloJunit becomes ** dependent </ font> ** form depending on the framework (Seasar2).

Product code


public class HelloJunit {

	GreetDao greetDao;
	
	public HelloJunit(GreetDao greetDao) {
		this.greetDao = greetDao;
	}

	public String sayGreeting() {
		return greetDao.getGreet();
	}
}

For example, if you decide to move to Spring in the future, you will use @ Autowired when you DI with annotations. Therefore, it is easy to change to the framework by DI in the constructor.

Also, from the viewpoint of test code, if it is realized by annotation, it is necessary to specify @ RunWith (Seasar2.class) when declaring the class, but if it is DI in the constructor, new If you do, you can easily test.

Reference article

Practical clean architecture

Write code that is easy to write tests

When testing, code that is easy to write is important.

For example, the following code makes a conditional branch according to the result of the TimeJunit.getTime () static method, but it is difficult to replace the static method with a mock.

Using static method


public class ExampleJunit {
	public String sayGreeting() {
		int term = TimeJunit.getTime();
		switch(term) {
		case 1:
			return "pasta";
		case 2:
			return "ramen";
		default:
			return "rice";
		}
	}
}

The rest is hard to test even if you are inheriting and using superclass methods. For example, such logic that is often used in old systems.

Super class


public abstract class AbstractJunit {
	public String sayGreeting() {
		return this.getMessage();
	}
	abstract protected String getMessage();
}

Inheritance class


public class ExampleJunit extends AbstractJunit {
	protected String getMessage() {
		int term = 1;
		switch(term) {
		case 1:
			return "pasta";
		case 2:
			return "ramen";
		default:
			return "rice";
		}
	}
}

In this example, it can be tested, but in the case where inheritance is actually used in business, the superclass is a god class (a convenient class to put logic in anything) and it is a spaghetti source. It's awkward because I have to do reflection in the test.

Think about whether you really should use inheritance and implement it.

Reference article

Code that is easy to write tests is good code

Other references

http://s2container.seasar.org/2.4/ja/easyMock.html

Recommended Posts

Test code using mock with JUnit (EasyMock center)
Unit test with Junit.
[JUnit 5 compatible] Write a test using JUnit 5 with Spring boot 2.2, 2.3
Test Web API with junit
I got an InvalidUseOfMatchersException when using any with JUnit Mock
Test Spring framework controller with Junit
Control test order in Junit4 with enumeration
Try using Spring Boot with VS Code
How to test private scope with JUnit
JUnit 5 gradle test fails with lombok annotation
Java automated test implementation with JUnit 5 + Gradle
[Java] How to test for null with JUnit
[CircleCI 2.0] [Java] [Maven] [JUnit] Aggregate JUnit test results with CircleCI 2.0
Test controller with Mock MVC in Spring Boot
How to execute and mock methods using JUnit
Environment construction procedure for using PowerMock with JUnit
Java automated test implementation with JUnit 5 + Apache Maven
How to test interrupts during Thread.sleep with JUnit
Easy JUnit test of Elasticsearch 2018 version with embedded-elasticsearch
How to write test code with Basic authentication
Make System.out a Mock with Spock Test Framework
Tips for testing with mock for classes using @value
Mixin test cases with JUnit 5 and default methods
Efficient test code
Getting started with Java programs using Visual Studio Code
How to unit test with JVM with source using RxAndroid
Implement test doubles in JUnit without using external libraries
WebAPI unit test and integration test with SpringBoot + Junit5, 4 patterns
I wrote a test with Spring Boot + JUnit 5 now