您现在的位置是:首页 > 正文

mock进行单元测试经验之谈

2024-04-01 01:23:00阅读 2

1、单元测试

在这里插入图片描述
前言

我们在购买电脑的时候,一次就可以开机了,这是因为在出厂的时候厂家就帮我们做好了测试。那如果没有厂家这步,我们会面临显示器无法显示的问题,磁盘无法挂载等情况。那运气好可能一次就能定位,运气不好,还需要排查显示器,内存条,主板,显卡等一系列组件。等我们排查就花费了大量的时间和精力。那如果在组装之前就测试好了每个组件情况,也就能避免这样的事情发生了。如果把电脑的生产,测试和软件的开发测试类比,就会发现。
显卡,内存条就像是软件中的单元,通常是函数或者类,对单个元器件的测试就像是软件测试中的单元测试;
组装完成的功能机箱,显示器就像是软件中的模块,对机箱显示器的测试就像是软件中的集成测试; 电脑全部组装完成就像是软件完成了预发布版本。
电脑全部组装完成后的开机测试就像是软件中的系统测试。

1.1、定义

单元测试是指对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作,这里的最小可测试单元通常是指函数或者类。

  • 驱动代码是用来调用被测函数的,而桩代码和 Mock 代码是用来代替被测函数调用的真实代码的。

  • Stub(桩对象):Stub通常用于替代测试对象的某些部分,以便进行单元测试等测试。例如,当被测代码需要访问外部数据源或者调用其他函数时,我们可以使用Stub来模拟这些依赖项的行为,从而使得测试过程更加独立和可控。

  • Mock(模拟对象):Mock通常用于模拟函数或对象的行为,以便更好地进行单元测试或功能测试。例如,当被测代码需要与某个对象进行交互时,我们可以使用Mock来模拟该对象的行为和响应,并判断被测代码的行为是否正确。

1.2、作用

一般测试方法如下:

  • 启动整个应用,模拟用户正常操作。设计到大量的改动需要再次模拟场景。
  • 代码某个地方写一个临时入口(比如main),模拟调用。临时代码用后需要删除。

当有如下场景的时候就可以考虑采用单元测试

  • 测试场景较多,且需要多次场景测试。(比如每次改动一个点需要多次调用模拟场景,这里适合做成自动化。)
  • 被测单元依赖的模块尚未开发完成A,而被测单元需要依赖模块的返回值进行后续处理。
  • 需要将当前被测单元和其依赖模块独立开来,构造一个独立的测试环境,不关注被测单元的依赖对象,只关注被测单元的功能逻辑。(比如新增了逻辑不需要测试整个流程,只需要测试修改部分逻辑)
  • 被测单元依赖的对象较难模拟或者构造比较复杂。(比如db连接池)

1.3、使用

1.3.1、 常用注解

  • @SpringBootTest:获取启动类,加载配置,寻找主配置启动类(被 @SpringBootApplication 注解

  • @RunWith(SpringRunner.class):让JUnit运行Spring的测试环境,获得Spring环境的上下文的支持

  • @Test:测试方法,可以测试期望异常(配置expected )和超时时间。

  • @Mock :是 Mockito.mock() 方法的简写。创建的是全部mock的对象,即在对具体的方法打桩之前,mock对象的所有属性和方法全被置空(0或null)。

  • @Spy:会调用真实的方法,有返回值的调用真实方法并返回真实值;如果发现修饰的变量是 null,会自动调用类的无参构造函数来初始化。定义了mock方法的则执行mock(即虚假函数);默认生成后所有依赖的对象都会null,且要一个无参构造。

  • @InjectMocks :创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。如果使用spring的@Autowired注解一起使用,则会直接使用spring容器的对象,并将@Mock(或@Spy)对象注入。

  • @MockBean : Spring Boot 中的注解。我们可以使用 @MockBean 将 mock 对象添加到 Spring 应用程序上下文中。该 mock 对象将替换应用程序上下文中任何现有的相同类型的 bean。如果应用程序上下文中没有相同类型的 bean,它将使用 mock 的对象作为 bean 添加到上下文中。

  • @SpyBean:同上。

1.3.2、 注意事项

  1. @InjectMocks由mock框架管理,所以只能注入@Mock和@Spy的对象。
@Mock
AService aService;

@InjectMocks
AController aController;  //这里会注aService

@Autowired
AController aController;//这里不会注aService

class BController{
    AService aService;
}
  1. @MockBean和@SpyBean由spring管理,会替换上下文相同对象。
@MockBean
AService aService;

@Autowired
AController aController; //这里会注入aService
  1. 如果想一个spring对象注入mock框架的对象,可通过@InjectMocks桥接。
@Mock
AService aService;

@Autowired
@InjectMocks
AController aController;//这里会注入aService
  1. @SpyBean存在循环依赖问题,其原因主要是早期暴露和正常暴露会创建不同对象,造成对象不一致。通过如下方式也没办法解决,因为spy的是spring增强的对象,而不是像@SpyBean注解代理的是原生对象。
AService bean = context.getBean(AService.class);
AService spy = Mockito.spy(bean);
  1. 设置 spy 逻辑时不能再使用 when(某对象.某方法).thenReturn(某对象) 的语法,而是需要使用 doReturn(某对象).when(某对象).某方法 或者 doNothing(某对象).when(某对象).某方法

  2. 对于 static 、 final 、private修饰的方法和equals()、hashCode()方法, Mockito 无法对其进行when(…).thenReturn(…) 操作。

1.3.4、 注解使用场景

  • 非spring环境:@Mock+@Spy+@InjectMocks
  • spring环境:@MockBean+@SpyBean+@Autowired,为测试主体类部分打桩考虑使用SpyBean, 为外部依赖打桩,考虑使用MockBean

测试代码

@Service
public class AService {
public String hasReturnAndArgs(String str){
    return "10";
}
public String hasReturn(){
    return "10";
}

public void hasArgs(String str){
    System.out.println(1000);
}
public void noArgs(){
    System.out.println(1000);
}
}

@RestController
public class AController {
    @Autowired
    private AService aService;
public String hasReturnAndArgs(String str){
    return aService.hasReturnAndArgs(str);
}
public String hasReturn(){
    return aService.hasReturn();
}

public void hasArgs(String str){
    aService.hasArgs(str);
}
public void noArgs(){
    aService.noArgs();
}
}

1.3.5、使用mock

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class ServiceTest {
    @Before
    public void before() {
        // 启用 Mockito 注解
        MockitoAnnotations.initMocks(this);
    }
    
    @Mock
    AService aService;
    @InjectMocks
    AController aController;
    @Test
    public void test() {
        //1.不调用真实方法,默认返回null
        String value = aService.hasReturnAndArgs("10");
        Assert.assertEquals(value, null);

        //2.打桩
        //当传参是10L时,返回response
        Mockito.when(aService.hasReturnAndArgs("10")).thenReturn("30");
        //当传参是20L时,真实调用
        Mockito.when(aService.hasReturnAndArgs("20")).thenCallRealMethod();
        //当传参是30L时,抛出异常
        Mockito.when(aService.hasReturnAndArgs("30")).thenThrow(new Exception("test error"));

        Assert.assertEquals(aService.hasReturnAndArgs("10"), "30");
        //入口为真实方法,内部mock对象调用的也是mock方法
        Assert.assertNotEquals(aService.hasReturnAndArgs("20"), "30");
        try {
            Assert.assertNotEquals(aService.hasReturnAndArgs("30"), "30");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        //3.注入对象
        Assert.assertEquals(aController.hasReturnAndArgs("10"), "30");
    }
}

1.3.6、使用Spy

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class ServiceTest {
    @Before
    public void before() {
        // 启用 Mockito 注解
        MockitoAnnotations.initMocks(this);
    }
 
    @Spy
    AService spy;
    @Test
    public void test() {
        //AService spyTemp = new AService();
        //AService spy = Mockito.spy(spyTemp);

        //1.调用真实方法
        Assert.assertEquals(spy.hasReturnAndArgs("20"), "10");

        //2.打桩
        Mockito.doReturn("30").when(spy).hasReturnAndArgs("20");
        Assert.assertEquals(spy.hasReturnAndArgs("20"), "30");
        //验证是否被调用了一次
        Mockito.verify(spy,times(1)).hasReturnAndArgs("20");


        //设置任何hasReturnAndArgs调用都返回30
        Mockito.doReturn("30").when(spy).hasReturnAndArgs(Mockito.anyString());
        Assert.assertEquals( spy.hasReturnAndArgs("-2"), "30");
        Mockito.verify(spy,times(2)).hasReturnAndArgs(Mockito.anyString());



        //不支持这样
        Mockito.when(spy.hasReturnAndArgs("20")).thenReturn("10");
        Assert.assertEquals(spy.hasReturnAndArgs("20"), "10");
    }
}

1.3.7、使用spring集成

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class ServiceTest {
    @Before
    public void before() {
        // 启用 Mockito 注解
        MockitoAnnotations.initMocks(this);
    }
  
    @SpyBean
    private AService spy;
    @Autowired
    AController aController;
    @Test
    public void test() {
        //调用真实方法
        Assert.assertEquals(spy.hasReturnAndArgs("20"), "10");
        Mockito.doReturn("30").when(spy).hasReturnAndArgs(Mockito.anyString());
        Assert.assertEquals(spy.hasReturnAndArgs("20"), "30");
        Mockito.verify(spy,times(1)).hasReturnAndArgs(Mockito.anyString());
        Assert.assertEquals(aController.hasReturnAndArgs("20"), "30");
    }
}

1.3.8、行为验证

        //是否调用过一次
        Mockito.verify(spy).hasReturnAndArgs(Mockito.anyString());
        //是否调用过N次
        Mockito.verify(spy,times(1)).hasReturnAndArgs(Mockito.anyString());
        //没有被调用,相当于 times(0)
        Mockito.verify(spy,never()).hasReturnAndArgs(Mockito.anyString());
        //atLeast(N) 至少被调用 N 次
        //atLeastOnce() 相当于 atLeast(1)
        //atMost(N) 最多被调用 N 次

1.3.9、断言

  @Test
    public void testAssert() {


        // allOf: 所有条件都必须满足,相当于&&
        assertThat("myname", allOf(startsWith("my"), containsString("name")));

        //  anyOf: 其中一个满足就通过, 相当于||

        assertThat("myname", anyOf(startsWith("na"), containsString("name")));

        //  both: &&

        assertThat("myname", both(containsString("my")).and(containsString("me")));

        //  either: 两者之一
        assertThat("myname", either(containsString("my")).or(containsString("you")));

        //  everyItem: 每个元素都需满足特定条件
        assertThat(Arrays.asList("my", "mine"), everyItem(startsWith("m")));

        //  hasItem: 是否有这个元素
        assertThat(Arrays.asList("my", "mine"), hasItem("my"));

        //  hasItems: 包含多个元素
        assertThat(Arrays.asList("my", "mine", "your"), hasItems("your", "my"));
        //  is: is(equalTo(x))或is(instanceOf(clazz.class))的简写
        assertThat("myname", is("myname"));
        assertThat("mynmae", is(String.class));

        //  anything(): 任何情况下,都匹配正确
        assertThat("myname", anything());

        //  not: 否为真,相当于!
        assertThat("myname", is(not("you")));

        //  nullValue(): 值为空
        String str = null;
        assertThat(str, is(nullValue()));

        //  notNullValue(): 值不为空
        String str2 = "123";

        assertThat(str2, is(notNullValue()));

//  -------------------------字符串匹配
        //  containsString:包含字符串
        assertThat("myname", containsString("na"));
        //  stringContainsInOrder: 顺序包含,“my”必须在“me”前面
        assertThat("myname", stringContainsInOrder(Arrays.asList("my", "me")));
        //  endsWith: 后缀
        assertThat("myname", endsWith("me"));
        //  startsWith: 前缀
        assertThat("myname", startsWith("my"));
        //  isEmptyString(): 空字符串
        assertThat("", isEmptyString());
        //  equalTo: 值相等, Object.equals(Object)
        assertThat("myname", equalTo("myname"));
        assertThat(new String[]{"a", "b"}, equalTo(new String[]{"a", "b"}));
        //  equalToIgnoringCase: 比较时,忽略大小写
        assertThat("myname", equalToIgnoringCase("MYNAME"));
        //  equalToIgnoringWhiteSpace: 比较时, 首尾空格忽略, 比较时中间用单个空格
        assertThat(" my \t name ", equalToIgnoringWhiteSpace(" my name "));
        //  isOneOf: 是否为其中之一
        assertThat("myname", isOneOf("myname", "yourname"));
        //  isIn: 是否为其成员
        assertThat("myname", isIn(new String[]{"myname", "yourname"}));
        //  toString() 返回值校验
        assertThat(333, hasToString(equalTo("333")));

//------------------------  数值匹配
        //  closeTo: [operand-error, operand+error], Double或BigDecimal类型
        assertThat(3.14, closeTo(3, 0.5));
         assertThat(new BigDecimal("3.14"), is(BigDecimalCloseTo.closeTo(new BigDecimal("3"), new BigDecimal("0.5"))));
        //  comparesEqualTo: compareTo比较值
        assertThat(2, comparesEqualTo(2));
        //  greaterThan: 大于
        assertThat(2, greaterThan(0));
        //  greaterThanOrEqualTo: 大于等于
        assertThat(2, greaterThanOrEqualTo(2));
        //  lessThan: 小于
        assertThat(0, lessThan(2));
        //  lessThanOrEqualTo: 小于等于
        assertThat(0, lessThanOrEqualTo(0));

//  -----------------------------------------------------集合匹配
        // array: 数组长度相等且对应元素也相等
        assertThat(new Integer[]{1, 2, 3}, is(array(equalTo(1), equalTo(2), equalTo(3))));
        // hasItemInArray: 数组是否包含特定元素
        assertThat(new String[]{"my", "you"}, hasItemInArray(startsWith("y")));
        // arrayContainingInAnyOrder, 顺序无关,长度要一致
        assertThat(new String[]{"my", "you"}, arrayContainingInAnyOrder("you", "my"));
        // arrayContaining:  顺序,长度一致
        assertThat(new String[]{"my", "you"}, arrayContaining("my", "you"));
        // arrayWithSize: 数组长度
        assertThat(new String[]{"my", "you"}, arrayWithSize(2));
        // emptyArray: 空数组
        assertThat(new String[0], emptyArray());
        // hasSize: 集合大小
        assertThat(Arrays.asList("my", "you"), hasSize(equalTo(2)));
        // empty: 空集合
        assertThat(new ArrayList<String>(), is(empty()));
        //  isIn: 是否为集合成员
        assertThat("myname", isIn(Arrays.asList("myname", "yourname")));

//  -------------------------------------------------Map匹配
        Map<String, String> myMap = new HashMap();
        myMap.put("name", "john");
        //  hasEntry: key && value匹配
        assertThat(myMap, hasEntry("name", "john"));
        //  hasKey: key匹配
        assertThat(myMap, hasKey(equalTo("name")));
        //  hasValue: value匹配
        assertThat(myMap, hasValue("john"));

    }

1.3.10、常用方法

               //有参有返回
        Mockito.doReturn("haha").when(spy).hasReturnAndArgs(Mockito.anyString());
        Assert.assertEquals(spy.hasReturnAndArgs("str"), "haha");
        //无参有返回
        Mockito.doReturn("hasReturn").when(spy).hasReturn();
        Assert.assertEquals(spy.hasReturn(), "hasReturn");
        //有参无返回
        Mockito.doNothing().when(spy).hasArgs(Mockito.anyString());
        spy.hasArgs("haha");
        Mockito.verify(spy).hasArgs(Mockito.anyString());
        //无参无返回
        Mockito.doNothing().when(spy).noArgs();
        spy.noArgs();
        Mockito.verify(spy).noArgs();
        //调用真实方法
        Mockito.doCallRealMethod().when(spy).hasReturnAndArgs("hha");
        Assert.assertEquals(spy.hasReturnAndArgs("hha"),"10");
        //静态方法

        //抛出异常
        Mockito.doThrow(new RuntimeException()).when(spy).hasReturnAndArgs(eq("20"));
        try {
            spy.hasReturnAndArgs("20");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        //---------------------------------------------------------其他
        //参数匹配器
        //anyInt()、anyString()、anyDouble()、anyList()、anyMap(),eq(1)
        Mockito.doReturn("haha").when(spy).hasReturnAndArgs(eq("20"));

        //模拟返回值
        Mockito.doAnswer(invocation -> {
            //获取参数值
            Object[] args = invocation.getArguments();
            String arg = (String) args[0];
            if ("hallo".equals(arg)){
                return "helloWorld";
            }
            return arg;
        }).when(spy).hasReturnAndArgs(Mockito.anyString());
        Assert.assertEquals(spy.hasReturnAndArgs("hallo"), "helloWorld");
        Assert.assertEquals(spy.hasReturnAndArgs("ha"), "ha");

1.3.11、mock静态方法

导入pom

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>4.0.0</version>
        </dependency>
        MockitoAnnotations.initMocks(this);
        Mockito.mockStatic(XXX.class).when(XXX::getXXX)
                .thenReturn("xxx");
                
                //如果用多次需要关闭
        try(MockedStatic<XXX> xx= Mockito.mockStatic(XXX.class)) {
            xx.when(() -> A.b(params)).thenReturn(null);
        }

1.4、统计覆盖率

在这里插入图片描述

红色为尚未覆盖的行,绿色为覆盖的行。class,method,line分别表示类/方法/行代码测试覆盖率。

在这里插入图片描述

网站文章

  • MySQL事物本质_【MySQL—原理】事务

    事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做,这就是事务的目的,也是事务模型区...

    2024-04-01 01:22:36
  • Multi-task 模型在推荐场景的一些应用和工作

    Multi-task 模型在推荐场景的一些应用和工作

    MMOE 左侧的shallow tower部分和右侧的main tower部分,论文中提到的采用类似Wide&amp;Deep模型结构就是指这两个tower,其中shallow tower可以对应Wi...

    2024-04-01 01:22:29
  • Vue(九)——页面路由(1)

    Vue(九)——页面路由(1)

    理解: 一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。前端路由:key是路径,value是组件。理解:value 是 component...

    2024-04-01 01:22:16
  • http:IP开头的路径使用js-audio-recorder报浏览器不支持getUserMedia

    http:IP开头的路径使用js-audio-recorder报浏览器不支持getUserMedia

    因为浏览器不支持http:IP开头的路径,认为安全性不高 只支持浏览器:file:,https:,http://localhost 解决办法: 在谷歌浏览器的地址栏中输入:chrome://flags/#unsafely-treat-insecure-origin-as-secure就好了,或者用http: 域名也可以解决 ...

    2024-04-01 01:21:51
  • 数组笔记

    js中用变量来代表一个值(数据),进行操作 怎么声明一个变量 怎么用变量代表一个值 怎么区别写的代码中,哪些是数据,哪些是变量 代码中除了值(数据)以外的”单词”,都是变量 变量的声明及赋值方式: 声明+赋值 ...

    2024-04-01 01:21:44
  • 零信任的基本概念【新航海】

    零信任的基本概念【新航海】

    「零信任」既不是技术也不是产品,而是一种安全理念。任何访问业务、数据、网络、代码的“源”都需要做验证,以此证时这个“源”是安全的后,再将其接入到需要访问的某个特定应用、数据、网络、代码上;在访问特定应用、数据、网络、代码的过程中,零信任会持续验证身份,直到停止使用或被中断。...

    2024-04-01 01:21:38
  • LeetCode--Python解析【Contains Duplicate】

    LeetCode--Python解析【Contains Duplicate】

    题目:方法:把给定list中的元素依次取出作为key值放入定义好的dict中,不需要value可赋None检查是否有重复的key存在class Solution: def containsDuplicate(self, nums): """ :type nums: List[int] :rtype: bool """ ...

    2024-04-01 01:21:12
  • [leetcode]#168. Excel Sheet Column Title

    题目翻译 给定一个正整数,返回其在Excel表格中作为列序号时对应的列标题。比如: 1 -> A 2 -> B 3 -> C … 26 -> Z 27 -> AA 28 -> AB思路方法 首先,我们要知道Excel里这个对应关系是什么样的。从A-Z对应1-26,当列标题进一位变成AA时,列对应的数字变成27。所以这个题本质上是一个10进制转26进制的问题,不过A对

    2024-04-01 01:21:06
  • Layui概述---学习笔记

    Layui概述---学习笔记

    Layui框架-图标组件&amp;按钮组件Layui概述Layui的获取Layui的目录结构Layui的引入 Layui概述 layui(谐音:类UI) 是一款采用自身模块规范编写的前端 UI 框架,...

    2024-04-01 01:20:58
  • 工作2年的java程序员怎么提高技术?

    工作2年的java程序员怎么提高技术?

    首先你基础要扎实.这里的基础不是说Java基础.而是算法,数据结构,典型的设计模式等原理并能用代码实现.这个开发两年了,可以重新温故一下了.这方面资料很多,可以到某些电商平台,搜索对应的热销书籍,或者...

    2024-04-01 01:20:33