头图

JUnit 5 单元测试实践

基本用法

  • 每个测试用例要有断言 assert
  • 每个用例只专注一件事情

Example

/**
 * @author 骚铭
 */
public interface TagFormatter {

    /**
     * 格式化标签值
     * @param value 标签值
     * @return      格式化结果
     */
    String format(Object value);

    /**
     * 如果 value 不合法,则返回空字符串,不抛出异常
     */
    default String safeFormat(Object value) {
        if (value == null) {
            return "";
        }

        try {
            return format(value);
        }
        catch (Exception e) {
            return "";
        }
    }
}
public class TagFormatterTest {
    @Test
    void testPercentage() {
        TagFormatter formatter = TagFormatterFactory.percentage();
        String result = formatter.format(99.3333);
        assertEquals(result, "99.33%");

        result = formatter.format(99.0);
        assertEquals(result, "99.00%");
    }

    @Test
    void testDecimal() {
        TagFormatter formatter = TagFormatterFactory.decimal();
        String result = formatter.format(99.3333);
        assertEquals(result, "99.33");

        result = formatter.format(99.3);
        assertEquals(result, "99.30");

        result = formatter.format(0.0);
        assertEquals(result, "0.00");
    }

    @Test
    void testDate() {
        TagFormatter formatter = TagFormatterFactory.date();
        String result = formatter.format(DateUtil.parseDate("2022-01-01"));
        assertEquals(result, "2022-01-01");
    }
}

Mock

使用 MockitoExtension,即可以使用 @Mock 注解。

import org.junit.jupiter.api.Assertions;  
import org.junit.jupiter.api.Test;  
import org.junit.jupiter.api.extension.ExtendWith;  
import org.mockito.Mock;  
import org.mockito.junit.jupiter.MockitoExtension;


@ExtendWith(MockitoExtension.class)  
public class MyTest {
    @Mock  
    private TaggingService taggingService;

    @Test
    public void test() {
        when(taggingService.tagging(eq("A"), any())).thenReturn(2);
        Assertions.assertEquals(taggingService.tagging("A"), 2);
    }
}

Spring

使用 SpringExtension,配合@MockBean,可以在需要注入的地方使用 mock bean。

import org.junit.jupiter.api.Test;  
import org.junit.jupiter.api.extension.ExtendWith;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.test.context.SpringBootTest;  
import org.springframework.boot.test.mock.mockito.MockBean;  
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)  
@SpringBootTest(classes = { NodeMapperImpl.class })  
public class UnitSerializeTest  {
    @Autowired
    NodeMapper nodeMapper;

    @MockBean
    TaggingService taggingService;

    @Test
    void test() {
        when(taggingService.tagging(any())).thenReturn(2);

        NodeDo nodeDo = new NodeDo();
        Node node = nodeMapper.toEntity(nodeDo);
    }

Embedded Redis

如果依赖了中间件或数据库,做单元测试时,有两种思路:

  • embedded service (基于内存)
  • TestsContainers(基于容器)

这里以 redis 为例,先探讨 embedded 的方式。需要添加如下测试依赖。

        <dependency>
            <groupId>it.ozimov</groupId>
            <artifactId>embedded-redis</artifactId>
            <version>0.7.2</version>
            <scope>test</scope>
        </dependency>

然后定义一个 RedisServer ,并用TestConfiguration 注解,仅作用于测试。

@TestConfiguration
public class TestRedisConfig {
    private final RedisServer redisServer = new RedisServer(6379);

    @PostConstruct
    public void postConstruct() {
        redisServer.start();
    }
    @PreDestroy
    public void preDestroy() {
        redisServer.stop();
    }
}

单元测试如下,classes 注意要添加上述类 TestRedisConfig

@Import(RedisConfig.class)
@SpringBootTest(classes = {
        ProductFieldServiceImpl.class,
        ProductFieldConverterImpl.class,
        LettuceConnectionFactory.class,
        TestRedisConfig.class
})
@ExtendWith(SpringExtension.class)
public class ProductFieldTest {

    @Autowired
    ProductFieldService productFieldService;

    @Test
    void getValue() {
        String value = productFieldService.getFormattedValue(ProductFieldValueQuery.of("WWW3037", "XZ_MF10"));
        System.out.println(value);
    }
}

Feign

对Feign接口进行单元测试时,需要 import 两个 configuration,因此可以抽一个基类出来,如下所示:

@ExtendWith(SpringExtension.class)
@Import({FeignAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class})
public class BaseFeignTest {

}

然后继承这个基类,并 enable 要测试的 client 即可。

@EnableFeignClients(clients = {ExampleFeign.class})
public class ProductFieldTest extends BaseFeignTest {

    @Autowired
    ExampleFeign feign;

    @Test
    void testFeign() {
        TagValuesDto result = feign.getProductFieldValue("WWW3037", "XZ_MF10").getData();
        System.out.println(result);
    }
}

DevOps

有了单元测试之后,我们希望每次编译部署都执行一次。此时只需将 maven -DskipTest=true 去掉,则会在每个发布的生命周期内执行单元测试。

一旦有测试用例不通过,就会阻断在自动化编译阶段。

对于屎山代码而言,可能会有些乱七八糟的test。此时如果我们只希望执行部分单元测试,要怎么做呢?

这个时候就需要在 pom.xmlbuild里面添加 surefire plugin,然后补充 excludes,即可跳过不希望执行的单元测试。示例如下。

<!-- Surefire Test-->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
    <configuration>
        <!--配置是否跳过测试用例执行-->
        <!--<skipTests>true</skipTests>-->
        <!-- 包含 -->
        <!--<includes>-->
        <!--<include>com.cmb.lr20.zxb.dialog.domain.*Test</include>-->
        <!--</includes>-->
        <!-- 排除 -->
        <excludes>
            <exclude>com/cmb/lr20/zxb/dialog/infrastructure/**/*.java</exclude>
            <exclude>com/cmb/lr20/zxb/dialog/infrastructure/*.java</exclude>
            <exclude>com/cmb/lr20/zxb/dialog/application/*.java</exclude>
            <exclude>com/cmb/lr20/zxb/dialog/external/**/*.java</exclude>
            <exclude>com/cmb/lr20/zxb/dialog/utils/*.java</exclude>
            <exclude>com/cmb/lr20/zxb/dialog/*ApplicationTest.java</exclude>
        </excludes>
    </configuration>
</plugin>

骚铭科技
1 声望2 粉丝

条路自己行,扑街唔好喊


引用和评论

0 条评论