说到单元测试,你会怎么做?

发布于:2024-05-09 ⋅ 阅读:(16) ⋅ 点赞:(0)

在软件开发的过程中,保证代码的质量与稳定性是至关重要的。而单元测试作为软件质量保证体系中的一环,扮演着不可或缺的角色。通过单元测试,我们可以确保代码在各种情况下都能按照预期工作,减少潜在的风险和问题。同时,单元测试也可以作为代码重构和优化的依据,帮助我们更好地理解代码的结构和逻辑。

在本文中,我们将介绍如何编写和执行JUnit单元测试,分享一些实用的技巧和实践

image.png

项目工程基于SpringBoot2.4 + Mybatis开发

1、准备工作

在pom.xml文件中加入以下依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter-test</artifactId>
    <version>2.1.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

2、Controller测试

在对应的Controller类中可以通过点击Go To -> Test来创建测试类,创建的测试类在src/test目录下,与Controller中类的package路径相同。

image.png 生成的类如下代码所示,加入RunWith、WebMvcTest注解,注入MockMvc来模拟Http请求。

RunWith 是JUnit测试框架中的一个重要注解,其主要作用是指定测试类使用的测试运行器(Test Runner)。在JUnit中,测试运行器负责管理测试的执行,并提供各种扩展和定制选项。

  • 参数化测试:使用@RunWith(Parameterized.class),我们可以对JUnit测试进行参数化,从而对同一测试方法使用不同的输入数据进行多次测试。这可以大大提高测试的效率和覆盖率;
  • Spring集成测试:使用@RunWith(SpringRunner.class)来集成Spring的功能,如自动注入、事务管理等。这使得我们可以在测试类中轻松地使用Spring的bean,而无需手动配置。
  • 自定义测试运行器:通过创建自定义的测试运行器类,并使用@RunWith注解指定它,我们可以实现更复杂的测试逻辑,如并行执行测试、分组执行测试等

WebMvcTest 可以指定需要测试的控制器类,并自动配置所需的bean,从而节省了测试时间。同时,你可以在测试过程中使用MockMvc对象进行请求的模拟和验证。 具体来说,@WebMvcTest注解的作用是专注于测试Spring MVC组件,而不是整个应用程序。它只扫描@Controller和@ControllerAdvice标注的Bean,以及一些其他和Web层相关的Bean,而不会启动整个Spring Boot应用。这样可以提高测试的效率,并减少测试的依赖关系。

MockMvc WebMvcTest注解会自动配置MockMvc,并加载控制器层相关的组件,如控制器、拦截器等。这使得在测试过程中,你可以方便地模拟HTTP请求,并验证控制器的响应是否符合预期。

@RunWith(SpringRunner.class)
@WebMvcTest(DemoController.class)
public class DemoControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private TestService testService;

    @BeforeEach
    void before() {
    }

    @AfterEach
    void tearDown() {
    }
    
   
    // 进行参数测试可以通过MockMvc模拟发起Http请求,返回的MvcResult来确定返回的请求码是否与你预想的一致。
    @Test
    void test_success() throws Exception {
        String json = "{"name":"test","code":"c001","id":1}";

        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/demo/test")
                // .header("token", token) 如果有Token可这样将Token加入请求中
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(json)
        ).andExpect(status().isOk()) // 判断Http请求是否成功
                .andDo(MockMvcResultHandlers.print()).andReturn();

        result.getResponse().setCharacterEncoding("utf-8");
        JSONObject jsonObject = JSON.parseObject(result.getResponse().getContentAsString());
        Assertions.assertEquals(jsonObject.get("code"), "200");
    }
    
    /*
      如果除了测试Http请求,还需要对返回数据进行测试的话,可以针对MvcResult做些处理。
      比如获取MvcResult中的response对象,将其转换成你返回的对象,而后通过Assertions.assertEquals对值进行比较判断
    */
    @Test
    void get_success() throws Exception {
        
        // 模拟service方法调用返回值
        String json = "{"name":"test","code":"c001","id":1}";
        when(testService.findById(any())).thenReturn(json);

        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/demo/test")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(json)
        ).andExpect(status().isOk())
                .andDo(MockMvcResultHandlers.print()).andReturn();

        result.getResponse().setCharacterEncoding("utf-8");
        JSONObject jsonObject = JSON.parseObject(result.getResponse().getContentAsString());
        Assertions.assertEquals(jsonObject.get("code"), "200");

        Assertions.assertEquals(result.getResponse().getContentAsString(), json);
    }
}

3、Service测试

service的测试基本上是逻辑代码测试,一些逻辑计算或复杂逻辑处理的代码,如果没有单元测试的帮助,在后续修改中简直就是修罗场,你没办法相信你修改的代码不会影响其他逻辑,也没办法保证修改后的逻辑一定是正确的。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoService.class)
public class DemoServiceTest {

    @Autowired
    private DemoService demoService;

    @MockBean
    private TestMapper mapper;

    @BeforeEach
    void before() {
    }

    @AfterEach
    void tearDown() {
    }

    @Test
    void edit_success() throws Exception {
        String json = "{"name":"test","code":"c001","id":1}";
        when(mapper.findById(any())).thenReturn(json);

        demoService.edit(json);
    }

    @Test
    void get_success() throws Exception {

    }
}

Service测试和Controller测试的区别在于类头使用的注解不太一样,一个使用的WebMvcTest另一个使用的SpringBootTest。但是Service中为了实现某些目的,使用了代理又该如何测试呢,如下:

@Service("demo")
public class DemoServiceImpl {

    /**
     * 获取当前service代理对象
     *
     * @return 代理对象
     */
    private DemoServiceImpl self() {
        return (DemoServiceImpl) AopContext.currentProxy();
    }


    public Integer testA() {
        System.out.println("A");
        return self().testB();
    }


    public Integer testB() {
        System.out.println("B");
        return 2;
    }
}

那就只能通过mockito来模拟Spring的上下文,获取到一个代理类来实现测试,具体参照另一篇文章

4、Dao测试

Dao层的测试可以说是最关键的,如果你的Sql语句都执行不了,那你前面写的controller、service都是在扯淡。Dao的测试既想真正连接数据库测试,又不希望启动整个工程来测试,且测试完的数据能自动回滚就最好了!

MybatisTest它不是MyBatis框架直接提供的官方注解,但是基于mybatis-spring-boot-starter-test 框架的一个自定义注解,它的主要作用是帮助开发者在测试时只加载与MyBatis相关的beans,而不需要加载整个应用上下文。

  • 性能提升:由于只加载了与MyBatis相关的beans,因此启动测试的速度会更快,因为不需要加载和初始化整个应用上下文。
  • 专注度提高:由于只包含与MyBatis相关的beans,因此你可以更专注于测试MyBatis的映射器和SQL语句,而不必担心其他应用组件的影响。

AutoConfigureTestDatabase注解用于自动配置用于测试的数据库, Replace的枚举有三个值:

  • NONE:不替换主数据源。测试将使用在应用程序配置中定义的数据源。
  • ANY(默认值):替换任何存在的数据源为内存数据库(如 H2)。
  • AUTO:仅在检测到类路径上有内存数据库时(如 H2 或 Derby)才替换数据源。

ActiveProfiles注解用于在有多个配置文件的时候,指定需要使用的配置文件。

@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("dao-dev")
class CurrencyMapperTest extends AbstractTransactionalJUnit4SpringContextTests {

    @Resource
    private CurrencyMapper mapper;

    @Test
    void test() {
        Currency currency = new Currency();
        currency.setName("name");

        mapper.save(currency);

        // 断言保存后的数据,重新从数据库获取后是否一致
        Currency saveCurrency = mapper.findById(currency.getId());
        Assertions.assertNotNull(saveCurrency.getId());

        
        currency.setUpdateUserName("lucas");
        currency.setName("name1");
        mapper.edit(currency);
        
        // 断言保存后的数据,重新从数据库获取后是否一致
        Currency editCurrency = mapper.findById(currency.getId());
        Assertions.assertEquals(editCurrency.getName(), currency.getName());
        Assertions.assertEquals(editCurrency.getUpdateUserName(), currency.getUpdateUserName());
    }
}

Dao测试使用也可以使用RunWith(SpringRunner.class)+SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)来实现,但是这种方式会加载一些不需要的依赖进来。比如项目中有使用Flowable时,启动单元测试会去检查Flowable的依赖,如果项目时多Module的形式来进行的,极有可能导致单元测试无法启动,但使用MybatisTest+AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)可以避免这种情况的发生