Spring Boot集成单元测试之如何mock

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分别表示类/方法/行代码测试覆盖率。

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值