一、简介

1、概述

模拟是单元测试的重要组成部分,使用Mockito库可以轻松编写干净直观的Java代码单元测试。

2、关键术语

  • Mock

模拟对象(空壳实例),所有方法默认返回值(0/false/null)。

@Mock

Mockito.mock(Foo.class)
  • Stub

定义Mock对象当收到某种调用时模拟返回什么。

when(mock.bar()).thenReturn(x)

doReturn(x).when(mock).bar()
  • Verify

验证某方法是否被调用、调用的几次等。

verify(mock).bar()

verify(mock, times(2)).bar()
  • Spy

部分模拟,未Stub的方法真执行,Stub的模拟执行。

@Spy
spy(realObject)

3、依赖

<dependencies>
	<dependency>
		<groupId>org.mockito</groupId>
		<artifactId>mockito-core</artifactId>
		<version>4.11.0</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter</artifactId>
		<version>5.10.2</version>
		<scope>test</scope>
	</dependency>
</dependencies>

二、功能

1、模拟方法

  • 被模拟的类
public class MyList extends AbstractList<String> {

    @Override
    public String get(int index) {
        return null;
    }

    @Override
    public int size() {
        return 1;
    }
}
  • 简单模拟
MyList listMock = mock(MyList.class);
when(listMock.add(anyString())).thenReturn(false);

boolean added = listMock.add("abc");
//验证方法被调用
verify(listMock).add(anyString());
//验证返回值是否设置的一样
assertFalse(added);

mock时也可以指定名称,方便调试。

MyList listMock = mock(MyList.class, "myMock");
  • 模拟回答(Answer)

自定义的Answer类:

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<String>{

	@Override
	public String answer(InvocationOnMock invocation) throws Throwable {
		return "Hello";
	}

}

测试:

MyList listMock = mock(MyList.class, new CustomAnswer());
String value = listMock.get(1);
verify(listMock).get(anyInt());
assertTrue("Hello".equals(value));
  • 模拟配置(MockSettings)
MockSettings settings = withSettings().defaultAnswer(RETURNS_SMART_NULLS);
MyList listMock = mock(MyList.class, settings);
String value = listMock.get(3);
verify(listMock).get(anyInt());
assertTrue("".equals(value));

以上例子中相关方法通过静态引入:

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_SMART_NULLS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

import org.junit.jupiter.api.Test;
import org.mockito.MockSettings;

2、参数匹配

  • 被模拟的类
public interface FlowerService {

	public String analyze(String name);
	
	public boolean isBigFlower(String name, int size);
}
  • 指定参数时的返回值
FlowerService service = Mockito.mock(FlowerService.class);
//只有analyze方法参数为poppy时返回Flower
doReturn("Flower").when(service).analyze("poppy");
assertEquals(null, service.analyze("abc"));
assertEquals("Flower", service.analyze("poppy"));
  • 指定任何参数返回固定值
FlowerService service = Mockito.mock(FlowerService.class);
//无论参数是什么都返回Flower
when(service.analyze(anyString())).thenReturn("Flower");
doReturn("Flower").when(service).analyze("poppy");
assertEquals("Flower", service.analyze("abc"));
assertEquals("Flower", service.analyze("poppy"));
  • 模拟多个参数的方法

如果一个方法有多个参数,不能用ArgumentMatchers只处理部分参数;Mockito要求必须通过匹配器或精确值提供所有参数。

例如:

FlowerService service = Mockito.mock(FlowerService.class);
when(service.isBigFlower("poppy", anyInt())).thenReturn(true);

会抛异常:

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at demo.ArgumentMatch.mock(ArgumentMatch.java:17)

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));

可以对第一个参数使用eq匹配器:

FlowerService service = Mockito.mock(FlowerService.class);
when(service.isBigFlower(eq("poppy"), anyInt())).thenReturn(true);
assertEquals(true, service.isBigFlower("poppy", 1));

3、抛出异常

  • 被模拟的类
import java.util.Map;

public class MyDictionary {

	private Map<String, String> wordMap;

	public void add(String word, String meaning) {
		wordMap.put(word, meaning);
	}

	public String getMeaning(String word) {
		return wordMap.get(word);
	}
}
  • 非空返回值方法

调用MyDictionary的getMeaning()方法时抛出NullPointerException:

MyDictionary dictMock = Mockito.mock(MyDictionary.class);
//使用when().thenThrow()
Mockito.when(dictMock.getMeaning(ArgumentMatchers.anyString())).thenThrow(NullPointerException.class);
assertThrows(NullPointerException.class, () -> dictMock.getMeaning("word"));
  • 空返回值方法
MyDictionary dictMock = Mockito.mock(MyDictionary.class);
//使用doThrow()
Mockito.doThrow(IllegalStateException.class).when(dictMock).add(ArgumentMatchers.anyString(), ArgumentMatchers.anyString());
assertThrows(IllegalStateException.class, () -> dictMock.add("word", "meaning"));
  • 使用异常对象

可以将异常类换为一个异常对象,此处以doThrow()方式为例:

MyDictionary dictMock = Mockito.mock(MyDictionary.class);
Mockito.doThrow(new IllegalStateException("Error occurred.")).when(dictMock).add(ArgumentMatchers.anyString(), ArgumentMatchers.anyString());
assertThrows(IllegalStateException.class, () -> dictMock.add("word", "meaning"));

4、注解

启用Mockito注解:

@BeforeEach
public void init() {
	MockitoAnnotations.openMocks(this);
}
  • @Mock
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

public class AnnotationTest {

	@Mock
	private List<String> mockedList;
	
	@BeforeEach
	public void init() {
	    MockitoAnnotations.openMocks(this);
	}
	
	@Test
	public void mockTest() {
		mockedList.add("one");
	    Mockito.verify(mockedList).add("one");
	    assertEquals(0, mockedList.size());
	    Mockito.when(mockedList.size()).thenReturn(100);
	    assertEquals(100, mockedList.size());
	}
}
  • @Spy

使用@Spy注解来监视已有的实例:

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

public class AnnotationTest {

	@Spy
	private List<String> spiedList = new ArrayList<>();
	
	@BeforeEach
	public void init() {
	    MockitoAnnotations.openMocks(this);
	}
	
	@Test
	public void mockTest() {
		spiedList.add("one");
	    spiedList.add("two");

	    Mockito.verify(spiedList).add("one");
	    Mockito.verify(spiedList).add("two");

	    assertEquals(2, spiedList.size());

	    Mockito.doReturn(100).when(spiedList).size();
	    assertEquals(100, spiedList.size());
	}
}
  • @InjectMocks
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Map;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

public class AnnotationTest {

	@Mock
	private Map<String, String> wordMap;

	@InjectMocks
	private MyDictionary dictionary = new MyDictionary();
	
	@BeforeEach
	public void init() {
	    MockitoAnnotations.openMocks(this);
	}
	
	@Test
	public void mockTest() {
		Mockito.when(wordMap.get("word")).thenReturn("meaning");
	    assertEquals("meaning", dictionary.getMeaning("word"));
	}
}

5、参数捕获

参数捕获器(ArgumentCaptor)用来捕获调用时传入的参数,方便进一步断言。

样例中使用到的类如下:

  • Format

邮件格式类:

public enum Format {

	TEXT_ONLY, HTML;
}
  • Email

邮件类:

public class Email {

	private String to;
	private String subject;
	private String body;
	private Format format;

	public Email(String to, String subject, String body) {
		super();
		this.to = to;
		this.subject = subject;
		this.body = body;
	}

	public String getTo() {
		return to;
	}

	public void setTo(String to) {
		this.to = to;
	}

	public String getSubject() {
		return subject;
	}

	public void setSubject(String subject) {
		this.subject = subject;
	}

	public String getBody() {
		return body;
	}

	public void setBody(String body) {
		this.body = body;
	}

	public Format getFormat() {
		return format;
	}

	public void setFormat(Format format) {
		this.format = format;
	}

}
  • DeliveryPlatform

发送平台:

public interface DeliveryPlatform {

	void deliver(Email email);

}
  • EmailService

邮件服务:

public class EmailService {

	private DeliveryPlatform platform;

    public EmailService(DeliveryPlatform platform) {
        this.platform = platform;
    }

    public void send(String to, String subject, String body, boolean html) {
        Format format = Format.TEXT_ONLY;
        if (html) {
            format = Format.HTML;
        }
        Email email = new Email(to, subject, body);
        email.setFormat(format);
        platform.deliver(email);
    }

}

场景:

检查发送的邮件格式是否为HTML格式,因此需要捕获并检查传递给platform.deliver()方法的参数email的值。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class CaptorTest {

	@Mock
    private DeliveryPlatform platform;

    @InjectMocks
    private EmailService emailService;
    
    @Captor
    private ArgumentCaptor<Email> emailCaptor;
    
	@BeforeEach
	public void init() {
	    MockitoAnnotations.openMocks(this);
	}
	
	@Test
	public void test() {
		String to = "to@test.com";
	    String subject = "Using ArgumentCaptor";
	    String body = "Hello World!";
	    emailService.send(to, subject, body, true);
		
		//捕获email
		verify(platform).deliver(emailCaptor.capture());
		//获取捕获的值
		Email emailCaptorValue = emailCaptor.getValue();
		assertEquals(Format.HTML, emailCaptorValue.getFormat());
	}
}
参考资料