什么是接口测试 Mock?
接口测试过程中,针对某些难以构造或无法直接访问的服务,需采用 Mock 服务来模拟远程服务的行为,从而实现接口测试、开发与调试。通过使用 Mock 方式来完成数据的模拟。 Mock 是指运用各类手段模拟出所需资源以供测试之用。 Mock 的资源一般具有如下特征:
- 被测目标对该资源存在依赖。
- 该资源可能因多种原因而不稳定,其返回结果可能不断变化,或者并非总能获取到。
- 该资源与被测目标自身质量无关。
- 这些资源与被测目标自身指令无关。
- 这些资源可能是外部或底层接口、一个系统、一组数据对象,亦或是一整套目标软件的工作环境等。
- 利用 Mock 通过的测试与在真实环境下通过的测试终究存在一定差别
工具 - 实现
工具:
- 直接基于 JDK
- Mockito
- JavaFaker
搭建 Mock Server:
方式 01 - 直接基于 JDK
创建接口
- 返回基本数据类型和一个自定义数据类型
java
public interface JdkServices {
boolean getBooleanResult();
short getShortResult();
int getIntegerResult();
long getLongResult();
char getCharResult();
User getUser();
}
JDKMockServiceProxy
实现 InvocationHandler
返回代理
- 通过 JDK 动态代理 + 反射获取方法返回值
- 如果返回值类型为定义的基本类型,返回 Mock 模拟数据
- 否则抛出异常
java
public class JDKMockServiceProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?> returnType = method.getReturnType();
System.out.println("Mock return type:" + returnType);
return mockReturnType(returnType);
}
public Object mockReturnType(Class<?> returnType) {
//返回基本类型模拟数据
if (returnType.isPrimitive()) {
if (returnType == boolean.class) {
return false;
} else if (returnType == short.class) {
return (short) 0;
} else if (returnType == int.class) {
return 0;
} else if (returnType == long.class) {
return 0L;
} else if (returnType == char.class) {
return 'a';
}
}
throw new RuntimeException("Only support to mork primitive type");
}
}
测试
java
public class JDKMockServiceProxyTest {
private JdkServices services;
@Before
public void setUp() {
this.services = (JdkServices) Proxy.newProxyInstance(
this.getClass().getClassLoader(),
new Class[]{JdkServices.class},
new JDKMockServiceProxy()
);
}
@Test
public void testServicesMethods() {
System.out.println(services.getCharResult()); //'a'
System.out.println(services.getBooleanResult()); //false
System.out.println(services.getIntegerResult()); // 0
System.out.println(services.getLongResult()); //0L
System.out.println(services.getShortResult()); //0
System.out.println(services.getUser()); //Exception
}
}
执行结果
方式 02 - Java Faker
参考
示例 - Mock 数据常用方法
导入依赖
xml
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>{Your version - 最新 1.0.2}</version>
</dependency>
方法 - 模拟 IP + 域名
java
public class TestJavaFaker {
@Test
public void testFaker() {
Faker faker = new Faker();
//支持本地化,参考 https://github.com/DiUS/java-faker
faker = new Faker(new Locale("zh-CN"));
//mock 域名 + IP
String domainName = faker.internet().domainName();
String ipv4 = faker.internet().ipV4Address();
String ipv6 = faker.internet().ipV6Address();
System.out.println(domainName);
System.out.println(ipv4);
System.out.println(ipv6);
//生成密码
//password(int minimumLength, int maximumLength, boolean includeUppercase)
String password = faker.internet().password(10, 20, true);
System.out.println(password);
}
}
本地化可配置参数
输出
方法 - bothify()
将 ?
替换为随机字母,#
替换为随机数字
java
@Test
public void testWhenBothifyCalled_checkPatternMatch() {
FakeValuesService service = new FakeValuesService(new Locale("zh-CN"), new RandomService());
//bothify() 方法,将 ? 替换为字母,# 替换为数字
String email = service.bothify("????##@gmail.com");
System.out.println(email);
Matcher emailMatcher = Pattern.compile("\w{4}\d{2}@gmail.com").matcher(email);
Assert.assertTrue(emailMatcher.find());
}
输出
方法 - address()
java
@Test
public void testFakerClass() {
Faker faker = new Faker();
String streetName = faker.address().streetName();
String number = faker.address().buildingNumber();
String city = faker.address().city();
String country = faker.address().country();
System.out.println(String.format("%s\n%s\n%s\n%s",
number,
streetName,
city,
country));
String creator = faker.programmingLanguage().name();
System.out.println(creator);
}
方法 03 - Mockito (常用于单测)
参考
导入依赖
xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>{your version}</version>
<scope>test</scope>
</dependency>
示例 - Mock Dao 层
创建 DemoUserDao
java
public class DemoUserDao {
public String getName() {
return "Elon Mask";
}
}
创建 DemoUserService
java
public class DemoUserService {
DemoUserDao demoUserDao;
public String getUserName() {
return this.demoUserDao.getName();
}
}
测试类
java
package com.jools.rpc.mockito;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import static org.junit.Assert.*;
public class DemoUserServiceTest {
@Test
public void setUp() throws Exception {
//Mock Dao instnce
DemoUserDao mockDao = Mockito.mock(DemoUserDao.class);
//打桩
Mockito.when(mockDao.getName()).thenReturn("Elon Mask - AAA");
//调用该方法,结果仅会返回 Elon Mask - AAA
Assert.assertEquals(mockDao.getName(), "Elon Mask - AAA");
System.out.println(mockDao.getName());
//构建 mockService
DemoUserService mockService = new DemoUserService(mockDao);
//同样仅返回 Elon Mask - AAA
Assert.assertEquals(mockService.getUserName(), "Elon Mask - AAA");
System.out.println(mockService.getUserName());
}
}
输出
获取 Mock instance 方式
1. mock(Class<?> clazz) 方法得到 Mock Instance
java
@Test
public void testMockClass() {
Random mockRandom = mock(Random.class);
//返回值默认式该类型的默认值
Assert.assertFalse(mockRandom.nextBoolean());
Assert.assertEquals(0, mockRandom.nextInt());
Assert.assertEquals(0.0, mockRandom.nextDouble());
//打桩,后续始终返回 -1
when(mockRandom.nextLong()).thenReturn(-1L);
Assert.assertEquals(-1L, mockRandom.nextLong());
Assert.assertNotEquals(0L, mockRandom.nextLong());
}
java
@Test
public void testMockClass() {
// 使用 PowerMockito 模拟 final 类
Random mockRandom = PowerMockito.mock(Random.class);
// 配置模拟行为
Mockito.when(mockRandom.nextInt()).thenReturn(42);
// 测试模拟行为
int result = mockRandom.nextInt();
System.out.println("Mocked Random.nextInt(): " + result); // 输出: 42
}
2. @Mock 注解 得到 Mock instance
遵循匹配条件的最新声明的匹配
java
@RunWith(MockitoJUnitRunner.class)
public class MockAnnotationTest {
@Mock
private List<String> list;
@Test
public void testMockAnnotation() {
when(list.get(0)).thenReturn("A");
Assert.assertEquals("A", list.get(0));
when(list.get(0)).thenReturn("B");
Assert.assertEquals("B", list.get(0));
//模糊匹配
when(list.get(anyInt())).thenReturn("CCC");
Assert.assertEquals("CCC", list.get(1));
Assert.assertEquals("CCC", list.get(3));
Assert.assertEquals("CCC", list.get(5));
}
}
注意 - 需要添加 @RunWith(MockitoJUnitRunner.class)
初始化使用 Mock 注释的模拟,因此不需要显式使用 MockitoAnnotations. initMocks(对象)。在每个测试方法之前初始化 Mocks
常用方法
校验调用次数 verify()
java
@Test
public void testVerify() {
list.add("once");
list.add("twice");
list.add("twice");
list.add("Three times");
list.add("Three times");
list.add("Three times");
verify(list).add("once");
verify(list, times(1)).add("once");
verify(list, times((2))).add("twice");
verify(list, times((3))).add("Three times");
//never() 校验调用次数为 0 = times(0)
// public static VerificationMode never() {
// return times(0);
// }
verify(list, never()).add("Never added");
//校验至少 / 至多
verify(list, atLeastOnce()).add("Three times");
verify(list, atLeast(2)).add("twice");
verify(list, atMost(3)).add("Three times");
}
Mock 抛出异常 doThrow()
java
@Test
public void testMockException() {
doThrow(new RuntimeException("Can not be cleared")).when(list).clear();
try {
list.clear();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
输出
校验添加顺序/调用情况 InOrder
java
@Test
public void testOrder() {
//Single Mock
list.add("Add first");
list.add("Add second");
InOrder inOrder = inOrder(list);
inOrder.verify(list).add("Add first");
inOrder.verify(list).add("Add second");
//Multi mock
List list2 = mock(List.class);
list2.add("New added first");
list.add("New added second");
inOrder = inOrder(list2, list);
inOrder.verify(list2).add("New added first");
inOrder.verify(list).add("New added second");
//verify that inOrder is not interacted
inOrder.verifyNoMoreInteractions();
}
校验连续调用
java
@Test
public void testConsecutiveCalls() {
when(list.get(anyInt()))
.thenThrow(new RuntimeException("First call throws exception"))
.thenReturn("foo");
//第一次调用抛出 RuntimException
try {
list.get(1);
} catch (Exception e) {
System.out.println(e.getMessage());
}
//第二次调用及以后返回 foo
System.out.println(list.get(1));
System.out.println(list.get(1));
}
输出