单元测试-SpringBoot Test和Mock

单元测试-SpringBoot Test和Mock

“单元测试”

“junit,mock,桩”

1. 什么是单元测试

定义:是指对软件中的最小可测试单元进行检查和验证。

Java里单元指一个方法。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

2. 单元测试与Spring Boot

2.1 引入依赖spring-boot-starter-test

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-testartifactId>
    <scope>testscope>
dependency>
spring-boot-starter-test中包含了junit和mockito等依赖

​​单元测试-SpringBoot Test和Mock_第1张图片

2.2 相关依赖

  • junit – 标准的单元测试Java应用程序
  • Spring Test & Spring Boot Test – 对Spring Boot应用程序的单元测试提供支持
  • Mockito, Java mocking框架,用于模拟任何Spring管理的Bean,比如在单元测试中模拟一个第三方系统Service接口返回的数据,而不会去真正调用第三方系统;
  • AssertJ,一个流畅的assertion库,同时也提供了更多的期望值与测试返回值的比较方式;
  • JSONassert,对JSON对象或者JSON字符串断言的库。
  • …………

2.3 标准的Spring Boot单元测试结构

@DisplayName("AlarmMsgstationController测试类")  //起别名
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Test {
}


3. SpringBoot Test常用注解

单元测试-SpringBoot Test和Mock_第2张图片

4. 基本用法

类上添加注解,启动Spring Boot环境

@RunWith(SpringRunner.class)
@SpringBootTest
public class FirstTest {
    @Test
    public void test() {
        int a=1;
        Assertions.assertEquals(1,a);//判断二者是否相等
    }
}

可以使用Assertions类来判断结果是否符合预期;

4.1 直接注入

对数据访问层(Service层同理)

单元测试-SpringBoot Test和Mock_第3张图片
单元测试-SpringBoot Test和Mock_第4张图片
ps:@Autowired直接注入的方法会真实操作数据库,如果在单元测试中不想改变数据数据库中的值,不能使用直接注入的方法

其实可以在类上再添加这两个注解,通过@Transactional可以知道调用了数据库,对其操作进行回滚

但是如果项目中使用了@Component注解(在SpringBoot项目启动的时候就会跟着实例化/启动),@Component注解的类里有多线程方法,那么在执行单元测试的时候,由于多线程任务的影响,就可能对数据库造成了数据修改,即使使用了事务回滚注解@Transactional。(我在百度上看到的,没找到具体的测试方法,所以没试)

@Component注解:带此注解的类看为组件,当使用基于该注解的配置和类路径扫描的时候,这些类就会被实例化。

@Transactional
@Rollback(true) // 事务自动回滚,默认是true。可以不写

4.2 Mock注入

实现原理:使用Stub(桩)技术动态的替换原程序的功能。

直接跑Java代码,不需要启用Spring及连接数据库,模拟一切操作数据库的步骤,不执行任何SQL,也可以模拟任何返回值

4.2.1 使用Mock的优点:
  1. 可以完全脱离数据库
  2. 只针对某一个小方法(一个小的单元)来测试,测试过程中,不需要启动其他的东西,不免其他因素可能产生的干扰
4.2.2 编写Mock代码
  1. 不再使用@Autowired

    启动Spring会导致运行单元测试的时候的速度变慢(run->Junit Test),单元测试只针对某一个类的方法来测试,不需要启动Spring,只需要对应的实体实例就够了,在需要注入bean的时候直接new

  2. 不再使用@SpringBootTest

  3. 不调用数据库

    @Transactional @Rollback(true)这两个注解也不要

  4. 使用Assert断言

基本应用:

mock 对象的方法的返回值默认都是返回类型的默认值

import org.junit.Assert;
import org.junit.Test;
import java.util.Random;

import static org.mockito.Mockito.*;

public class MockitoDemo {

    @Test
    public void test() {
        Random mockRandom = mock(Random.class); //mock了一个Random对象
	Assert.assertEquals(0, mockRandom.nextInt());//未进行打桩,每次返回值都是0
   
 	when(mockRandom.nextInt()).thenReturn(100);  // 进行打桩操作,指定调用 nextInt 方法时,永远返回 100
        Assert.assertEquals(100, mockRandom.nextInt());
    }
}
4.2.3 Mock的注解和常用的方法
@Mock

@Mock 注解可以理解为对 mock 方法的一个替代。使用该注解时,要使用MockitoAnnotations.initMocks​ 方法,让注解生效。旧版的是initMocks,新版的是openMocks

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Random;

import static org.mockito.Mockito.*;

public class MockitoDemo {

    @Mock
    private Random random;

    @Before
    public void before() {
        // 让注解生效
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test() {
        when(random.nextInt()).thenReturn(100);
        Assert.assertEquals(100, random.nextInt());
    }

}

也可以用MockitoJUnitRunner​来代替MockitoAnnotations.initMocks

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.Random;

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class MockitoDemo {

    @Mock
    private Random random;

    @Test
    public void test() {
        when(random.nextInt()).thenReturn(100);
        Assert.assertEquals(100, random.nextInt());
    }

}
@Spy

mock()方法与spy()方法的不同:

  1. 被spy的对象会走真实的方法,而mock对象不会
  2. spy方法的参数是对象实例,mock的参数是class
@InjectMocks

mockito 会将 @Mock​、@Spy​ 修饰的对象自动注入到 @InjectMocks​ 修饰的对象中

thenReturn

thenReturn 用来指定特定函数和参数调用的返回值;

thenReturn 中可以指定多个返回值。在调用时返回值依次出现。若调用次数超过返回值的数量,再次调用时返回最后一个返回值。

doReturn 的作用和 thenReturn 相同,但使用方式不同:

when(mockRandom.nextInt()).thenReturn(1);//返回值为1
when(mockRandom.nextInt()).thenReturn(1, 2, 3);

doReturn(1).when(random).nextInt();
thenThrow

thenThrow 用来让函数调用抛出异常。(可搭配try catch使用)

thenThrow 中可以指定多个异常。在调用时异常依次出现。若调用次数超过异常的数量,再次调用时抛出最后一个异常。

when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常"));
when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常1"), new RuntimeException("异常2"));


    @Test
    public void test() {
        Random mockRandom = mock(Random.class);
        when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常1"), new RuntimeException("异常2"));

        try {
            mockRandom.nextInt();
            Assert.fail();//上一行会抛出异常,到catch中去,走不到这里
        } catch (Exception ex) {
            Assert.assertTrue(ex instanceof RuntimeException);
            Assert.assertEquals("异常1", ex.getMessage());
        }

        try {
            mockRandom.nextInt();
            Assert.fail();
        } catch (Exception ex) {
            Assert.assertTrue(ex instanceof RuntimeException);
            Assert.assertEquals("异常2", ex.getMessage());
        }


    }

对应返回类型是 void 的函数,thenThrow 是无效的,要使用 doThrow。也可以用 doThrow 让返回非void的函数抛出异常

doThrow(new RuntimeException("异常")).when(exampleService).hello();

// 下面这句等同于 when(random.nextInt()).thenThrow(new RuntimeException("异常"));
doThrow(new RuntimeException("异常")).when(random).nextInt();
reset

使用 reset 方法,可以重置之前自定义的返回值和异常。

reset(exampleService);
vetify

使用 verify 可以校验 mock 对象是否发生过某些操作,配合 time 方法,可以校验某些操作发生的次数

//判断backOutstockMapper.selectReportCountByMap()方法是否被调用1次
verify(backOutstockMapper, times(1)).selectReportCountByMap(Mockito.any());

//校验backOutstockMapper.selectReportCountByMap()方法是否被调用过
verify(backOutstockMapper).selectReportCountByMap(Mockito.any());

4.2.5 断言
->assertTrue(String message, boolean condition)             要求condition == true
->assertFalse(String message, boolean condition)            要求condition == false
->assertEquals(String message, XXX expected,XXX actual) 要求expected期望的值能够等于actual
->assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
->assertNotNull(String message, Object object)          要求object!=null
->assertNull(String message, Object object)             要求object==null
->assertSame(String message, Object expected, Object actual)     要求expected == actual
->assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
->assertThat(String reason, T actual, Matcher matcher)  要求matcher.matches(actual) == true
->fail(String message) 要求执行的目标结构必然失败,同样要求代码不可达,即是这个方法在程序运行后不会成功返回,如果成功返回了则报错

4.3 Tips

  1. 对待类中私有的方法,可以用反射的方式进行测试

  2. 打包时跳过test

    mvn deploy -f pom_http.xml-jar -Dmaven.test.skip=true

  3. Mockito 默认是不支持静态方法,可使用 PowerMock 让 Mockito 支持静态方法(新增依赖)

5. 总结

单元测试测试的不是整条业务线,而是类中的单个方法单元。

按照单一性原则的话,一个方法只做一件事,那么针对这个方法的单元测试就简单了。

当多个方法单元测试的结果都没问题的时候,多个方法聚合成的业务链照理说也是没问题的,一个方法中依赖了其他方法的处理结果或返回结果,那么这个结果应当是可预测的,所以也是可以mock出所有场景的,而单元测试也应该覆盖到不同结果对应的场景。

单元测试除了测试代码逻辑外,最大的好处是可以检验整体设计是否合理。一个方法做了太多事的话,就会导致单元测试很难覆盖,比如service层的方法,如果入参的校验,业务逻辑的处理,不同数据表DB的操作,DB返回结果的校验处理全部在单一方法中实现,那对于后期业务的扩展、维护、问题的排查都不好进行;如果把以上说的那些全部分离出来,封装成一个个独立的方法,最后只在一个方法中总调,这样不仅单元测试比较好实现,而且后期的维护,扩展都会很容易。

ps:上面总结这段话不是我说的,是我在学习的过程中看到一位老哥写在评论区的。领导让我总结一下,我就抄过来敷衍领导了

你可能感兴趣的:(Java,单元测试,spring,boot,junit,Mock,springbootTest)