Java单元测试利器:Mockito模拟框架使用示例与最佳实践

Mockito模拟框架提供了不同的方式来模拟一个类。让我们来看一看通过这些方法我们可以模拟一个类并存根其行为。

使用Mockito进行方法模拟

我们可以使用Mockito类的mock()方法来创建给定类或接口的模拟对象。这是模拟对象最简单的方式。

package com.Olivia.mockito.mock;

import java.util.List;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class MockitoMockMethodExample {

	@SuppressWarnings("unchecked")
	@Test
	public void test() {
		// 使用Mockito.mock()方法
		List<String> mockList = mock(List.class);
		when(mockList.size()).thenReturn(5);
		assertTrue(mockList.size()==5);
	}
	
}

我们使用JUnit 5来编写测试用例,并结合Mockito来模拟对象。

Mockito的@Mock注解

我们也可以使用@Mock注解来模拟一个对象。这在我们想要在多个地方使用模拟对象时非常有用,因为我们避免了多次调用mock()方法。代码变得更易读,我们可以指定模拟对象的名称,这在出现错误时非常有用。当使用@Mock注解时,我们必须确保调用MockitoAnnotations.initMocks(this)来初始化模拟对象。我们可以在测试框架的设置方法中执行此操作,在测试之前执行。

package com.Olivia.mockito.mock;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

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

public class MockitoMockAnnotationExample {

	@Mock
	List<String> mockList;
	
	@BeforeEach
	public void setup() {
		// 如果我们不调用下面的方法,将会得到NullPointerException
		MockitoAnnotations.initMocks(this);
	}
	
	@SuppressWarnings("unchecked")
	@Test
	public void test() {
		when(mockList.get(0)).thenReturn("JournalDev");
		assertEquals("JournalDev", mockList.get(0));
	}
	
}

Mockito的@InjectMocks注解

当我们想将模拟对象注入另一个模拟对象时,我们可以使用@InjectMocks注解。@InjectMock会创建该类的模拟对象,并将被标记为@Mock的模拟对象注入其中。

package com.Olivia.mockito.mock;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

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

public class MockitoInjectMockAnnotationExample {

	@Mock
	List<String> mockList;
	
	//@InjectMock创建类的实例,并将标记为@Mock注解的模拟对象注入其中
	@InjectMocks
	Fruits mockFruits;
	
	@BeforeEach
	public void setup() {
		// 如果我们不调用下面的方法,将会得到NullPointerException
		MockitoAnnotations.initMocks(this);
	}
	
	@SuppressWarnings("unchecked")
	@Test
	public void test() {
		when(mockList.get(0)).thenReturn("Apple");
		when(mockList.size()).thenReturn(1);
		assertEquals("Apple", mockList.get(0));
		assertEquals(1, mockList.size());
		
		// mockFruits的names属性使用了mockList,下面的断言确认了这一点
		assertEquals("Apple", mockFruits.getNames().get(0));
		assertEquals(1, mockFruits.getNames().size());	
		
		mockList.add(1, "Mango");
		// 下面将打印null,因为mockList.get(1)没有被存根
		System.out.println(mockList.get(1));
	}
	
}

class Fruits{
	private List<String> names;

	public List<String> getNames() {
		return names;
	}

	public void setNames(List<String> names) {
		this.names = names;
	}
	
}

使用Mockito的spy()进行部分模拟

如果我们只想模拟特定行为并对未存根的行为调用真实方法,那么我们可以使用Mockito的spy()方法创建一个间谍对象。

package com.Olivia.mockito.mock;

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

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

import org.junit.jupiter.api.Test;

public class MockitoSpyMethodExample {

	@Test
	public void test() {
		List<String> list = new ArrayList<>();
		List<String> spyOnList = spy(list);
		
		when(spyOnList.size()).thenReturn(10);
		assertEquals(10, spyOnList.size());
		
		// 由于下面的方法没有被存根,所以调用真实方法
		spyOnList.add("Pankaj");
		spyOnList.add("Meghna");
		assertEquals("Pankaj", spyOnList.get(0));
		assertEquals("Meghna", spyOnList.get(1));
	}
	
}

Mockito @Spy注解

我们可以使用@Spy注解来对一个对象进行监视。让我们看一个简单的例子。

package com.Olivia.mockito.mock;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

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

public class MockitoSpyAnnotationExample {

	@Spy
	Utils mockUtils;
	
	@BeforeEach
	public void setup() {
		MockitoAnnotations.initMocks(this);
	}
	
	@Test
	public void test() {
		when(mockUtils.process(1, 1)).thenReturn(5);
		// 模拟的方法
		assertEquals(5, mockUtils.process(1, 1));
		// 由于没有被存根,调用了真实的方法
		assertEquals(20, mockUtils.process(19, 1));
		
	}
	
}

class Utils{
	public int process(int x, int y) {
		System.out.println("输入参数 = "+x+","+y);
		return x+y;
	}
}

请注意,@Spy注解试图调用无参构造函数来初始化模拟对象。如果你的类没有这个构造函数,那么你将会得到以下错误。

org.mockito.exceptions.base.MockitoException: 无法初始化标记为@Spy的字段'mockUtils'。
请确保'Utils'类型具有无参构造函数。
	at com.Olivia.mockito.mock.MockitoSpyAnnotationExample.setup(MockitoSpyAnnotationExample.java:18)

另外,请注意Mockito无法实例化内部类、局部类、抽象类和接口。因此,最好始终提供一个要监视的实例。否则,真实的方法可能不会被调用并被悄无声息地忽略掉。例如,如果您如下指定一个spy对象:

@Spy
List<String> spyList;

在调用add()或get()方法时,你会注意到真正的方法没有被调用。如果你像下面这样指定spy对象,那么一切都会正常工作。

@Spy
List<String> spyList = new ArrayList<>();

总结

Mockito模拟框架允许我们通过不同的方法和注解轻松创建模拟对象。我们还可以将一个模拟对象注入到另一个模拟对象中,这是一个非常有用的功能。

你可以在我们的GitHub存储库中查看更多的Mockito示例。

bannerAds