Mockito 教程:从入门到精通,掌握Java单元测试利器

这是文章《Mockito 教程》的第1部分(共1部分)。

Mockito 是一个基于 Java 的模拟框架,常与 JUnit 和 TestNG 等测试框架配合使用。它内部利用 Java 反射 API,能够创建服务对象。模拟对象返回虚拟数据,从而避免了对外部依赖的真实调用。通过模拟外部依赖并将其应用于待测试的代码,Mockito 极大地简化了测试的开发过程。

Mockito 教程

在本 Mockito 教程中,我们将使用 JUnit 5 来创建一些需要模拟的服务。

Mockito Maven 依赖

要在项目中实现基于 Mockito 的测试用例,您需要将以下依赖项添加到项目的 pom.xml 文件中:

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

请注意,对于 JUnit 5,需要使用 mockito-junit-jupiter 库。如果您使用的是其他测试框架,如 JUnit 4 或 TestNG,则应移除此依赖项,并仅包含 mockito-core 库。

Mockito 模拟对象的创建

Mockito 框架允许我们使用 @Mock 注解或 mock() 静态方法创建模拟对象。

Mockito 的 mock() 方法

下面的例子展示了 mock() 方法的使用。

package com.Olivia.mockito;

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

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

import com.Olivia.AddService;
import com.Olivia.CalcService;

public class CalcService1Test {

	@Test
	void testCalc() {
		System.out.println("**--- Test testCalc executed ---**");

		AddService addService;
		CalcService calcService;

		addService = Mockito.mock(AddService.class);
		calcService = new CalcService(addService);

		int num1 = 11;
		int num2 = 12;
		int expected = 23;

		when(addService.add(num1, num2)).thenReturn(expected);

		int actual = calcService.calc(num1, num2);

		assertEquals(expected, actual);

	}
}

在上述示例中,我们正在测试 CalcService。使用 Mockito.mock() 方法创建了一个 AddService 类的模拟对象。

Mockito 框架的 @Mock 注解

以下示例展示了 @Mock 注解的用法。

package com.Olivia.mockito;

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

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

import com.Olivia.AddService;
import com.Olivia.CalcService;

public class CalcService2Test {

	CalcService calcService;

	@Mock
	AddService addService;

	@BeforeEach
	public void setup() {
		MockitoAnnotations.initMocks(this);
	}

	@Test
	public void testCalc() {
		System.out.println("**--- Test testCalc executed ---**");

		calcService = new CalcService(addService);

		int num1 = 11;
		int num2 = 12;
		int expected = 23;

		when(addService.add(num1, num2)).thenReturn(expected);

		int actual = calcService.calc(num1, num2);

		assertEquals(expected, actual);

	}
}

请注意,我们需要调用 MockitoAnnotations.initMocks(this) 来初始化使用 @Mock@Spy@Captor@InjectMocks 注解的对象。

模拟行为验证 – Mockito

在使用 when()thenReturn() 函数时,可以为模拟类添加行为。这意味着当调用模拟对象 (addService) 的 add 方法并传入 (num1, num2) 参数时,它将返回存储在 expected 变量中的值。我们的 CalcService 类如下所示:

public class CalcService {
	
	private AddService addService;
	
	public CalcService(AddService addService) {
		this.addService = addService;
	}

	public int calc(int num1, int num2) {
		System.out.println("**--- CalcService calc executed ---**");
		return addService.add(num1, num2);
	}

}

CalcServiceAddService 类有一个依赖。它使用 AddService 类的 add 方法来执行操作。由于我们只想对 CalcService 类进行单元测试,所以我们必须模拟 AddService 实例。AddService 的接口如下:

public interface AddService {
	public int add(int num1, int num2);
}
public class AddServiceImpl implements AddService {
	@Override
	public int add(int num1, int num2) {
		System.out.println("**--- AddServiceImpl add executed ---**");
		return num1 + num2;
	}
}

Mockito 验证交互

Mockito 框架会跟踪所有方法调用及其传递给模拟对象的参数。Mockito 的 verify() 方法用于验证某个方法是否以指定的参数进行调用。我们还可以指定调用次数的逻辑,例如确切的调用次数、至少指定的调用次数、少于指定的调用次数等。我们可以使用 VerificationModeFactory 来处理调用次数的逻辑。Mockito 的 verify() 方法用于验证某个方法是否以正确的参数进行调用。它不像 assert 方法那样检查方法调用的结果。下面的示例展示了 verify() 方法的使用方式:

package com.Olivia.mockito;

import static org.mockito.Mockito.verify;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.internal.verification.VerificationModeFactory;

public class VerifyInteractionTest {
	@Test
	public void testMethod() {
		@SuppressWarnings("unchecked")
		List<String> mockedList = Mockito.mock(List.class);

		mockedList.add("first-element");
		mockedList.add("second-element");
		mockedList.add("third-element");
		mockedList.add("third-element");
		mockedList.clear();

		verify(mockedList).add("first-element");
		verify(mockedList).add("second-element");
		verify(mockedList, VerificationModeFactory.times(2)).add("third-element");

		verify(mockedList).clear();
	}

}

使用 Mockito 对具体类进行存根化

使用 when() - thenReturn() 函数,我们可以对具体/实现类和集合的单个元素进行存根操作。未存根的元素将包含空值。

package com.Olivia.mockito;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;

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

public class MockSingleElementTest {
	@SuppressWarnings("unchecked")
	@Test
	public void testMethod() {
		ArrayList mockedList = mock(ArrayList.class);

		when(mockedList.get(0)).thenReturn("first-element");

		System.out.println(mockedList.get(0));
		assertEquals("first-element", mockedList.get(0));
		
		// "null" gets printed as get(1) is not stubbed
		System.out.println(mockedList.get(1));
	}

}

Mockito Spy(间谍对象)

当您调用一个被监视对象(Spy)的方法时,除非预先定义了行为,否则将调用真实的方法。使用 Spy,我们可以通过 when()-thenReturn() 函数来定义行为,或者直接调用真实的实现。

package com.Olivia.mockito;

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 MockitoSpyTest {

	@Test
	public void testMethod() {
		List<String> list = new ArrayList<>();
		List<String> listSpy = spy(list);

		listSpy.add("first-element");
		System.out.println(listSpy.get(0));

		assertEquals("first-element", listSpy.get(0));
		when(listSpy.get(0)).thenReturn("second-element");
		System.out.println(listSpy.get(0));
		assertEquals("second-element", listSpy.get(0));
	}

}

结论

Mockito 是用于 Java 单元测试的流行模拟框架。我们可以很容易地使用 Mockito 对依赖进行模拟。Mockito 的编码风格流畅且类似于 JUnit 和 TestNG 框架,因此学习曲线很小。

您可以从我们的 GitHub 仓库下载完整的项目代码。

bannerAds