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.
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.
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.
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.
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.
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.
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--
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);
}
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);
}
EasyMock can only mock interface classes. If you want to mock a concrete class, try the inheritance method below.
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());
}
}
}
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.
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.
Code that is easy to write tests is good code
http://s2container.seasar.org/2.4/ja/easyMock.html
Recommended Posts