最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

【码农教程】手把手教你学会Mockito使用

来源:博客园

作者:京东零售 秦浩然


(资料图)

一、前期准备~

1、准备工作

    org.mockito    mockito-core    2.7.19    test    junit    junit    4.12    test

2、入门知识

1)Mockito:简单轻量级的做mocking测试的框架;2)mock对象:在调试期间用来作为真实对象的替代品;3)mock测试:在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试;4)stub:打桩,就是为mock对象的方法指定返回值(可抛出异常);5)verify:行为验证,验证指定方法调用情况(是否被调用,调用次数等);

3、五分钟入门Demo

@Testpublic void test0() {    //1、创建mock对象(模拟依赖的对象)    final List mock = Mockito.mock(List.class);    //2、使用mock对象(mock对象会对接口或类的方法给出默认实现)    System.out.println("mock.add result => " + mock.add("first"));  //false    System.out.println("mock.size result => " + mock.size());       //0    //3、打桩操作(状态测试:设置该对象指定方法被调用时的返回值)    Mockito.when(mock.get(0)).thenReturn("second");    Mockito.doReturn(66).when(mock).size();    //3、使用mock对象的stub(测试打桩结果)    System.out.println("mock.get result => " + mock.get(0));    //second    System.out.println("mock.size result => " + mock.size());   //66    //4、验证交互 verification(行为测试:验证方法调用情况)    Mockito.verify(mock).get(Mockito.anyInt());    Mockito.verify(mock, Mockito.times(2)).size();    //5、验证返回的结果(这是JUnit的功能)    assertEquals("second", mock.get(0));    assertEquals(66, mock.size());}

二、让我们开始学习吧!

1、行为验证

• 一旦mock对象被创建了,mock对象会记住所有的交互,然后你就可以选择性的验证你感兴趣的交互,验证不通过则抛出异常。

@Testpublic void test1() {    final List mockList = Mockito.mock(List.class);    mockList.add("mock1");    mockList.get(0);    mockList.size();    mockList.clear();    // 验证方法被使用(默认1次)    Mockito.verify(mockList).add("mock1");    // 验证方法被使用1次    Mockito.verify(mockList, Mockito.times(1)).get(0);    // 验证方法至少被使用1次    Mockito.verify(mockList, Mockito.atLeast(1)).size();    // 验证方法没有被使用    Mockito.verify(mockList, Mockito.never()).contains("mock2");    // 验证方法至多被使用5次    Mockito.verify(mockList, Mockito.atMost(5)).clear();    // 指定方法调用超时时间    Mockito.verify(mockList, timeout(100)).get(0);    // 指定时间内需要完成的次数    Mockito.verify(mockList, timeout(200).atLeastOnce()).size();}

2、如何做一些测试桩stub

• 默认情况下,所有的函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;

• 一旦测试桩函数被调用,该函数将会一致返回固定的值;

• 对于 static 和 final 方法, Mockito 无法对其 when(…).thenReturn(…) 操作。

@Testpublic void test2() {    //静态导入,减少代码量:import static org.mockito.Mockito.*;    final ArrayList mockList = mock(ArrayList.class);    // 设置方法调用返回值    when(mockList.add("test2")).thenReturn(true);    doReturn(true).when(mockList).add("test2");    System.out.println(mockList.add("test2"));  //true    // 设置方法调用抛出异常    when(mockList.get(0)).thenThrow(new RuntimeException());    doThrow(new RuntimeException()).when(mockList).get(0);    System.out.println(mockList.get(0));    //throw RuntimeException    // 无返回方法打桩    doNothing().when(mockList).clear();    // 为回调做测试桩(对方法返回进行拦截处理)    final Answer answer = new Answer() {        @Override        public String answer(InvocationOnMock invocationOnMock) throws Throwable {            final List mock = (List) invocationOnMock.getMock();            return "mock.size result => " + mock.size();        }    };    when(mockList.get(1)).thenAnswer(answer);    doAnswer(answer).when(mockList).get(1);    System.out.println(mockList.get(1));    //mock.size result => 0    // 对同一方法多次打桩,以最后一次为准    when(mockList.get(2)).thenReturn("test2_1");    when(mockList.get(2)).thenReturn("test2_2");    System.out.println(mockList.get(2));    //test2_2    System.out.println(mockList.get(2));    //test2_2    // 设置多次调用同类型结果    when(mockList.get(3)).thenReturn("test2_1", "test2_2");    when(mockList.get(3)).thenReturn("test2_1").thenReturn("test2_2");    System.out.println(mockList.get(3));    //test2_1    System.out.println(mockList.get(3));    //test2_2    // 为连续调用做测试桩(为同一个函数调用的不同的返回值或异常做测试桩)    when(mockList.get(4)).thenReturn("test2").thenThrow(new RuntimeException());    doReturn("test2").doThrow(new RuntimeException()).when(mockList).get(4);    System.out.println(mockList.get(4));    //test2    System.out.println(mockList.get(4));    //throw RuntimeException    // 无打桩方法,返回默认值    System.out.println(mockList.get(99));    //null}

3、参数匹配器

• 参数匹配器使验证和测试桩变得更灵活;

• 为了合理的使用复杂的参数匹配,使用equals()与anyX() 的匹配器会使得测试代码更简洁、简单。有时,会迫使你重构代码以使用equals()匹配或者实现equals()函数来帮助你进行测试;

• 如果你使用参数匹配器,所有参数都必须由匹配器提供;

• 支持自定义参数匹配器;

@Testpublic void test3() {    final Map mockMap = mock(Map.class);    // 正常打桩测试    when(mockMap.get("key")).thenReturn("value1");    System.out.println(mockMap.get("key"));     //value1    // 为灵活起见,可使用参数匹配器    when(mockMap.get(anyString())).thenReturn("value2");    System.out.println(mockMap.get(anyString()));   //value2    System.out.println(mockMap.get("test_key"));    //value2    System.out.println(mockMap.get(0)); //null    // 多个入参时,要么都使用参数匹配器,要么都不使用,否则会异常    when(mockMap.put(anyString(), anyInt())).thenReturn("value3");    System.out.println(mockMap.put("key3", 3));     //value3    System.out.println(mockMap.put(anyString(), anyInt()));     //value3    System.out.println(mockMap.put("key3", anyInt()));    //异常    // 行为验证时,也支持使用参数匹配器    verify(mockMap, atLeastOnce()).get(anyString());    verify(mockMap).put(anyString(), eq(3));    // 自定义参数匹配器    final ArgumentMatcher myArgumentMatcher = new ArgumentMatcher() {        @Override        public boolean matches(ArgumentTestRequest request) {            return "name".equals(request.getName()) || "value".equals(request.getValue());        }    };    // 自定义参数匹配器使用    final ArgumentTestService mock = mock(ArgumentTestService.class);    when(mock.argumentTestMethod(argThat(myArgumentMatcher))).thenReturn("success");    doReturn("success").when(mock).argumentTestMethod(argThat(myArgumentMatcher));    System.out.println(mock.argumentTestMethod(new ArgumentTestRequest("name", "value")));  // success    System.out.println(mock.argumentTestMethod(new ArgumentTestRequest()));     //null}

4、执行顺序验证

• 验证执行顺序是非常灵活的-你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可;

• 你可以仅通过那些需要验证顺序的mock对象来创建InOrder对象;

@Testpublic void test4() {    // 验证同一个对象多个方法的执行顺序    final List mockList = mock(List.class);    mockList.add("first");    mockList.add("second");    final InOrder inOrder = inOrder(mockList);    inOrder.verify(mockList).add("first");    inOrder.verify(mockList).add("second");    // 验证多个对象多个方法的执行顺序    final List mockList1 = mock(List.class);    final List mockList2 = mock(List.class);    mockList1.get(0);    mockList1.get(1);    mockList2.get(0);    mockList1.get(2);    mockList2.get(1);    final InOrder inOrder1 = inOrder(mockList1, mockList2);    inOrder1.verify(mockList1).get(0);    inOrder1.verify(mockList1).get(2);    inOrder1.verify(mockList2).get(1);}

5、确保交互(interaction)操作不会执行在mock对象上

• 一些用户可能会在频繁地使用verifyNoMoreInteractions(),甚至在每个测试函数中都用。但是verifyNoMoreInteractions()并不建议在每个测试函数中都使用;

• verifyNoMoreInteractions()在交互测试套件中只是一个便利的验证,它的作用是当你需要验证是否存在冗余调用时;

@Testpublic void test5() {    // 验证某个交互是否从未被执行    final List mock = mock(List.class);    mock.add("first");    verify(mock, never()).add("test5");   //通过    verify(mock, never()).add("first");  //异常    // 验证mock对象没有交互过    final List mock1 = mock(List.class);    final List mock2 = mock(List.class);    verifyZeroInteractions(mock1);  //通过    verifyNoMoreInteractions(mock1, mock2); //通过    verifyZeroInteractions(mock, mock2);  //异常    // 注意:可能只想验证前面的逻辑,但是加上最后一行,会导致出现异常。建议使用方法层面的验证,如:never();    //      在验证是否有冗余调用的时候,可使用此种方式。如下:    final List mockList = mock(List.class);    mockList.add("one");    mockList.add("two");    verify(mockList).add("one");    // 通过    verify(mockList, never()).get(0);    //通过    verifyZeroInteractions(mockList);   //异常}

6、使用注解简化mock对象创建

注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中:

MockitoAnnotations.initMocks(this);

也可以使用内置的runner: MockitoJUnitRunner 或者一个rule : MockitoRule;

// 代替 mock(ArgumentTestService.class) 创建mock对象;@Mockprivate ArgumentTestService argumentTestService;// 若改注解修饰的对象有成员变量,@Mock定义的mock对象会被自动注入;@InjectMocksprivate MockitoAnnotationServiceImpl mockitoAnnotationService;@Testpublic void test6() {    // 注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中;    MockitoAnnotations.initMocks(this);    when(argumentTestService.argumentTestMethod(new ArgumentTestRequest())).thenReturn("success");    System.out.println(argumentTestService.argumentTestMethod(new ArgumentTestRequest()));  //success    System.out.println(mockitoAnnotationService.mockitoAnnotationTestMethod()); //null}

7、监控真实对象(部分mock)

• 可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了;

• 尽量少使用spy对象,使用时也需要小心形式,例如spy对象可以用来处理遗留代码;

• stub语法中同样提供了部分mock的方法,可以调用真实的方法;

完全mock:

上文讲的内容是完全mock,即创建的mock对象与真实对象无关,mock对象的方法默认都是基本的实现,返回基本类型。可基于接口、实现类创建mock对象。

部分mock:

所谓部分mock,即创建的mock对象时基于真实对象的,mock对象的方法都是默认使用真实对象的方法,除非stub之后,才会以stub为准。基于实现类创建mock对象,否则在没有stub的情况下,调用真实方法时,会出现异常。

注意点:

Mockito并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互,不要期望从监控对象得到正确的结果。 当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果

@Testpublic void test7() {    // stub部分mock(stub中使用真实调用)。注意:需要mock实现类,否则会有异常    final StubTestService stubTestService = mock(StubTestServiceImpl.class);    when(stubTestService.stubTestMethodA("paramA")).thenCallRealMethod();    doCallRealMethod().when(stubTestService).stubTestMethodB();    System.out.println(stubTestService.stubTestMethodA("paramA"));  //stubTestMethodA is called, param = paramA    System.out.println(stubTestService.stubTestMethodB());  //stubTestMethodB is called    System.out.println(stubTestService.stubTestMethodC());  //null    // spy部分mock    final LinkedList linkedList = new LinkedList();    final LinkedList spy = spy(linkedList);    spy.add("one");    spy.add("two");    doReturn(100).when(spy).size();    when(spy.get(0)).thenReturn("one_test");    System.out.println(spy.size()); //100    System.out.println(spy.get(0)); //one_test    System.out.println(spy.get(1)); //two    // spy可以类比AOP。在spy中,由于默认是调用真实方法,所以第二种写法不等价于第一种写法,不推荐这种写法。    doReturn("two_test").when(spy).get(2);    when(spy.get(2)).thenReturn("two_test"); //异常 java.lang.IndexOutOfBoundsException: Index: 2, Size: 2    System.out.println(spy.get(2));   //two_test    // spy对象只是真实对象的复制,真实对象的改变不会影响spy对象    final List arrayList = new ArrayList<>();    final List spy1 = spy(arrayList);    spy1.add(0, "one");    System.out.println(spy1.get(0));    //one    arrayList.add(0, "list1");    System.out.println(arrayList.get(0));   //list1    System.out.println(spy1.get(0));    //one    // 若对某个方法stub之后,又想调用真实的方法,可以使用reset(spy)    final ArrayList arrayList1 = new ArrayList<>();    final ArrayList spy2 = spy(arrayList1);    doReturn(100).when(spy2).size();    System.out.println(spy2.size());    //100    reset(spy2);    System.out.println(spy2.size());    //0}

8、@Mock 和 @Spy的使用

• @Mock 等价于 Mockito.mock(Object.class);

• @Spy 等价于 Mockito.spy(obj);

区分是mock对象还是spy对象:Mockito.mockingDetails(someObject).isMock();Mockito.mockingDetails(someObject).isSpy();@Mockprivate StubTestService stubTestService;@Spyprivate StubTestServiceImpl stubTestServiceImpl;@Spyprivate StubTestService stubTestServiceImpl1 = new StubTestServiceImpl();@Testpublic void test8() {    MockitoAnnotations.initMocks(this);    // mock对象返回默认    System.out.println(stubTestService.stubTestMethodB());  //null    // spy对象调用真实方法    System.out.println(stubTestServiceImpl.stubTestMethodC());  //stubTestMethodC is called    System.out.println(stubTestServiceImpl1.stubTestMethodA("spy"));  //stubTestMethodA is called, param = spy    // 区分是mock对象还是spy对象    System.out.println(mockingDetails(stubTestService).isMock());   //true    System.out.println(mockingDetails(stubTestService).isSpy());    //false    System.out.println(mockingDetails(stubTestServiceImpl).isSpy());    //true}

9、ArgumentCaptor(参数捕获器)捕获方法参数进行验证。(可代替参数匹配器使用)

• 在某些场景中,不光要对方法的返回值和调用进行验证,同时需要验证一系列交互后所传入方法的参数。那么我们可以用参数捕获器来捕获传入方法的参数进行验证,看它是否符合我们的要求。

ArgumentCaptor介绍

通过ArgumentCaptor对象的forClass(Class

ArgumentCaptor的Api

argument.capture() 捕获方法参数

argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值

argument.getAllValues() 方法进行多次调用后,返回多个参数值

@Testpublic void test9() {    List mock = mock(List.class);    List mock1 = mock(List.class);    mock.add("John");    mock1.add("Brian");    mock1.add("Jim");    // 获取方法参数    ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);    verify(mock).add(argument.capture());    System.out.println(argument.getValue());    //John    // 多次调用获取最后一次    ArgumentCaptor argument1 = ArgumentCaptor.forClass(String.class);    verify(mock1, times(2)).add(argument1.capture());    System.out.println(argument1.getValue());    //Jim    // 获取所有调用参数    System.out.println(argument1.getAllValues());    //[Brian, Jim]}

10、简化 ArgumentCaptor 的创建

@Mockprivate List captorList;@Captorprivate ArgumentCaptor argumentCaptor;@Testpublic void test10() {    MockitoAnnotations.initMocks(this);    captorList.add("cap1");    captorList.add("cap2");    System.out.println(captorList.size());    verify(captorList, atLeastOnce()).add(argumentCaptor.capture());    System.out.println(argumentCaptor.getAllValues());}

11、高级特性:自定义验证失败信息

@Testpublic void test11() {    final ArrayList arrayList = mock(ArrayList.class);    arrayList.add("one");    arrayList.add("two");    verify(arrayList, description("size()没有调用")).size();    // org.mockito.exceptions.base.MockitoAssertionError: size()没有调用    verify(arrayList, timeout(200).times(3).description("验证失败")).add(anyString());    //org.mockito.exceptions.base.MockitoAssertionError: 验证失败}

12、高级特性:修改没有测试桩的调用的默认返回值

• 可以指定策略来创建mock对象的返回值。这是一个高级特性,通常来说,你不需要写这样的测试;

• 它对于遗留系统来说是很有用处的。当你不需要为函数调用打桩时你可以指定一个默认的answer;

@Testpublic void test12(){    // 创建mock对象、使用默认返回    final ArrayList mockList = mock(ArrayList.class);    System.out.println(mockList.get(0));    //null    // 这个实现首先尝试全局配置,如果没有全局配置就会使用默认的回答,它返回0,空集合,null,等等。    // 参考返回配置:ReturnsEmptyValues    mock(ArrayList.class, Answers.RETURNS_DEFAULTS);    // ReturnsSmartNulls首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回SmartNull。    // 如果最终返回对象,那么会简单返回null。一般用在处理遗留代码。    // 参考返回配置:ReturnsMoreEmptyValues    mock(ArrayList.class, Answers.RETURNS_SMART_NULLS);    // 未stub的方法,会调用真实方法。    //    注1:存根部分模拟使用时(mock.getSomething ()) .thenReturn (fakeValue)语法将调用的方法。对于部分模拟推荐使用doReturn语法。    //    注2:如果模拟是序列化反序列化,那么这个Answer将无法理解泛型的元数据。    mock(ArrayList.class, Answers.CALLS_REAL_METHODS);    // 深度stub,用于嵌套对象的mock。参考:https://www.cnblogs.com/Ming8006/p/6297333.html    mock(ArrayList.class, Answers.RETURNS_DEEP_STUBS);    // ReturnsMocks首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回mock。    // 如果返回类型不能mocked(例如是final)然后返回null。    mock(ArrayList.class, Answers.RETURNS_MOCKS);    //  mock对象的方法调用后,可以返回自己(类似builder模式)    mock(ArrayList.class, Answers.RETURNS_SELF);    // 自定义返回    final Answer answer = new Answer() {        @Override        public String answer(InvocationOnMock invocation) throws Throwable {            return "test_answer";        }    };    final ArrayList mockList1 = mock(ArrayList.class, answer);    System.out.println(mockList1.get(0));   //test_answer}

三、学习了这么多,牛刀小试一下!

测试实体类

@Datapublic class User {    /**     * 姓名,登录密码     */    

持久层DAO

public interface UserDao {    /**     * 根据name查找user     * @param name     * @return     */    User getUserByName(String name);    /**     * 保存user     * @param user     * @return     */    Integer saveUser(User user);}

业务层Service接口

public interface UserService {    /**     * 根据name查找user     * @param name     * @return     */    User getUserByName(String name);    /**     * 保存user     * @param user     * @return     */    Integer saveUser(User user);}

业务层Serive实现类

@Servicepublic class UserServiceImpl implements UserService {    //userDao    @Autowired    private UserDao userDao;    /**     * 根据name查找user     * @param name     * @return     */    @Override    public User getUserByName(String name) {        try {            return userDao.getUserByName(name);        } catch (Exception e) {            throw new RuntimeException("查询user异常");        }    }    /**     * 保存user     * @param user     * @return     */    @Override    public Integer saveUser(User user) {        if (userDao.getUserByName(user.getName()) != null) {            throw new RuntimeException("用户名已存在");        }        try {            return userDao.saveUser(user);        } catch (Exception e) {            throw new RuntimeException("保存用户异常");        }    }}

现在我们的Service写好了,想要单元测试一下,但是Dao是其他人开发的,目前还没有写好,那我们如何测试呢?

public class UserServiceTest {    /**     * Mock测试:根据name查询user     */    @Test    public void getUserByNameTest() {        // mock对象        final UserDao userDao = mock(UserDao.class);        final UserServiceImpl userService = new UserServiceImpl();        userService.setUserDao(userDao);        // stub调用        final User user = new User();        user.setName("admin");        user.setPassword("pass");        when(userDao.getUserByName("admin")).thenReturn(user);        // 执行待测试方法        final User user1 = userService.getUserByName("admin");        System.out.println("查询结果:" + JacksonUtil.obj2json(user1));  //查询结果:{"name":"admin","password":"pass"}        // 验证mock对象交互        verify(userDao).getUserByName(anyString());        // 验证查询结果        Assert.assertNotNull("查询结果为空!", user1);        Assert.assertEquals("查询结果错误!", "admin", user1.getName());    }    /**     * Mock测试:保存user     */    @Mock    private UserDao userDao;    @InjectMocks    private UserServiceImpl userService;    @Test    public void saveUserTest() throws Exception{        // 执行注解初始化        MockitoAnnotations.initMocks(this);        // mock对象stub操作        final User user = new User();        user.setName("admin");        user.setPassword("pass");        when(userDao.getUserByName("admin")).thenReturn(user).thenReturn(null);        when(userDao.saveUser(any(User.class))).thenReturn(1);        // 验证用户名重复的情况        try {            userService.saveUser(user);            throw new Exception();  //走到这里说明验证失败        } catch (RuntimeException e) {            System.out.println("重复用户名保存失败-测试通过");   //重复用户名保存失败-测试通过        }        verify(userDao).getUserByName("admin");        // 验证正常保存的情况        user.setName("user");        final Integer integer = userService.saveUser(user);        System.out.println("保存结果:" + integer);  //保存结果:1        Assert.assertEquals("保存失败!", 1, integer.longValue());        verify(userDao).saveUser(any(User.class));        verify(userDao, times(2)).getUserByName(anyString());    }}

根据以上代码我们可以知道,当我们的待测类开发完成而依赖的类的实现还没有开发完成。此时,我们就可以用到我们的Mock测试,模拟我们依赖类的返回值,使我们的待测类与依赖类解耦。这样,我们就可以对我们的待测类进行单元测了。

四、参考文档及进一步学习~

Mockito英文版javadoc:https://javadoc.io/static/org.mockito/mockito-core/3.3.3/org/mockito/Mockito.htmlMockito中文文档(部分):https://blog.csdn.net/bboyfeiyu/article/details/52127551#35Mockito使用教程:https://www.cnblogs.com/Ming8006/p/6297333.html参数捕获器使用:https://www.journaldev.com/21892/mockito-argumentcaptor-captor-annotation利用ArgumentCaptor(参数捕获器)捕获方法参数进行验证:https://www.iteye.com/blog/hotdog-916364改变mock返回值:https://www.huangyunkun.com/2014/10/25/mockito-deep-stub-with-enum/五分钟了解Mockito:https://www.iteye.com/blog/liuzhijun-1512780使用Mockito进行单元测试:https://www.iteye.com/blog/qiuguo0205-1443344JUnit + Mockito 单元测试:https://blog.csdn.net/zhangxin09/article/details/42422643Mockito中@Mock与@InjectMock:https://www.cnblogs.com/langren1992/p/9681600.htmlmockito中两种部分mock的实现,spy、callRealMethod:https://www.cnblogs.com/softidea/p/4204389.htmlMockito 中被 Mocked 的对象属性及方法的默认值:https://www.cnblogs.com/fnlingnzb-learner/p/10635250.html单元测试工具之Mockito:https://blog.csdn.net/qq_32140971/article/details/90598454引入Mockito测试用@Spy和@Mock:https://blog.csdn.net/message_lx/article/details/83308114Mockito初探(含实例):https://www.iteye.com/blog/sgq0085-2031319测试覆盖率统计:https://blog.csdn.net/lvyuan1234/article/details/82836052?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task测试覆盖率无法统计解决:https://blog.csdn.net/zhanglei082319/article/details/81536398

关键词: 验证方法 单元测试