项目测试 - 哪些工具可以实现测试 Mock?

什么是接口测试 Mock?

接口测试过程中,针对某些难以构造或无法直接访问的服务,需采用 Mock 服务来模拟远程服务的行为,从而实现接口测试、开发与调试。通过使用 Mock 方式来完成数据的模拟。 Mock 是指运用各类手段模拟出所需资源以供测试之用。 Mock 的资源一般具有如下特征:

  1. 被测目标对该资源存在依赖。
  2. 该资源可能因多种原因而不稳定,其返回结果可能不断变化,或者并非总能获取到。
  3. 该资源与被测目标自身质量无关。
  4. 这些资源与被测目标自身指令无关。
  5. 这些资源可能是外部或底层接口、一个系统、一组数据对象,亦或是一整套目标软件的工作环境等。
  6. 利用 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);
    }
}

本地化可配置参数

参考 - Supported Locales

输出

方法 - 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)

MockitoJUnitRunner - Doc

初始化使用 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));
    }

输出

相关推荐
vx-程序开发1 分钟前
django医院预约挂号系统---附源码23353
java·javascript·spring boot·python·eclipse·django·php
金斗潼关3 分钟前
ysoserial的使用
java
ruleslol7 分钟前
volatile 关键字
java
Codelinghu27 分钟前
AI 写代码越强,程序员越不能只懂代码
后端
卡卡敲码32 分钟前
Skills 撞车了,Agent 怎么选
后端
羑悻32 分钟前
周一早上三件事砸过来,我以为要延期,结果 AI 替我扛了一半!
后端
文艺理科生38 分钟前
3 年,8 种方案,1 次重构:LangChain 记忆方案如何从混乱走向清晰
前端·后端·架构
lilian23344 分钟前
Harmony os 技术实战|拼豆制图10:把取消、解析失败和保存失败写成可恢复状态机
java·javascript·华为·harmonyos
Ai拆代码的曹操1 小时前
Agent 做错了怎么办?Self-Critique 机制拆解
后端·agent·ai编程
天性1 小时前
AgentScope Java 源码深读:一次请求如何触发工具、Middleware 和子 Agent
后端