项目测试 - 哪些工具可以实现测试 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));
    }

输出

相关推荐
运维&陈同学1 小时前
【Elasticsearch05】企业级日志分析系统ELK之集群工作原理
运维·开发语言·后端·python·elasticsearch·自动化·jenkins·哈希算法
ZHOUPUYU2 小时前
最新 neo4j 5.26版本下载安装配置步骤【附安装包】
java·后端·jdk·nosql·数据库开发·neo4j·图形数据库
Q_19284999063 小时前
基于Spring Boot的找律师系统
java·spring boot·后端
ZVAyIVqt0UFji4 小时前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
谢家小布柔4 小时前
Git图形界面以及idea中集合Git使用
java·git
loop lee4 小时前
Nginx - 负载均衡及其配置(Balance)
java·开发语言·github
smileSunshineMan4 小时前
vertx idea快速使用
java·ide·intellij-idea·vertx
阿乾之铭4 小时前
IntelliJ IDEA中的语言级别版本与目标字节码版本配置
java·ide·intellij-idea
SomeB1oody5 小时前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
toto4125 小时前
线程安全与线程不安全
java·开发语言·安全