一、创建单元测试service

右键service->new->other->Junit Test Case->next 选择需要创建单元测试的方法或者直接点击finish
文章最后附上相关依赖

二、示例代码

// 取dev环境配置文件
@ActiveProfiles("dev")
// 指定启动类
@SpringBootTest(classes = ProjectApplication.class)
// 如果想在测试中使用Spring测试框架功能(例如)@MockBean,则必须使用@ExtendWith(SpringExtension.class)。
// 它取代了不推荐使用的JUnit4@RunWith(SpringJUnit4ClassRunner.class)
@ExtendWith(SpringExtension.class)
public class AppApplicationManagerServiceTest {

 	// 需要加了@SpyBean或@MockBean注解才可以模板方法返回值
    // @SpyBean注解如果没有模拟方法返回,会调用真实方法
    @SpyBean
    private AppApplicationManagerService appApplicationManagerService;
    
    // @MockBean注解如果没有模拟方法返回,会返回null
    @MockBean
    private CommonService commonService;

    @BeforeEach // 每个测试方法之前都会执行一次
    public void setUp() {
        when(commonService.getOsreIdByCode(Mockito.anyString())).thenReturn(107);
    }

    // 最终结果显示名称
    @DisplayName("根据租户编码+应用编码获取应用管理员列表")
    // 参数化测试,可以与@CsvSource、@EnumSource、@MethodSource等注解结合使用
    @ParameterizedTest
    // 参数以逗号分隔,以此为例,会执行三次这个测试方法,参数分别对应
    @CsvSource({ "dev002, SP-CI, hrdashboard", "dev002, CDPLIFE, hrdashboard", "dev002, third, hrdashboard" })
    public void testGetApplicationManager(String tenantCode, String appCode, String sysCode) {
          // 如果调用了appRoleMapper.selectByExample(any())方法,参数为任意类型对象,会返回null
           // 与数据库相关操作或调用其他微服务操作可以使用这个方法模拟返回值
           when(appRoleMapper.selectByExample(any())).thenReturn(null);
        // 执行测试方法
        List<String> resList = appApplicationManagerService.getApplicationManager(tenantCode, appCode, sysCode);
        if ("SP-CI".equals(appCode)) {
            boolean judge = DataUtils.isEmptyList(resList);
            // 断言,每个测试方法断言最好大于等于1
            assertTrue(judge);
        }
    }
}

三、注解

1、@SpyBean、@MockBean、@Autowired
  • 如果不需要模拟方法返回值,可以使用@Autowired或@SpyBean
  • 如果既需要调用真实方法,也需要模拟方法返回值,只能使用@SpyBean
  • 如果只需要模拟方法返回值,可以使用@MockBean或@SpyBean
2、@ExtendWith(SpringExtension.class)

如果想在测试中使用Spring测试框架功能(例如)@MockBean,则必须使用@ExtendWith(SpringExtension.class)。它取代了不推荐使用的JUnit4@RunWith(SpringJUnit4ClassRunner.class)

3、@BeforeEach、@BeforeAll、@AfterAll、@AfterEach
  • @BeforeEach:每个测试方法执行之前都会执行
  • @AfterEach:每个测试方法执行结束都会执行
  • @BeforeAll:所有测试方法执行之前会执行
  • @AfterAll所有测试方法执行之后会执行
4、@DisplayName()

运行结果中该方法显示的名称

5、@ParameterizedTest、@RepeatedTest()

参数化测试,使用时需要替换掉@Test,同时需要最少增加一个源,该源将为每个调用提供参数,然后使用测试方法中的参数。如

@ParameterizedTest
@CsvSource({ "dev002, SP-CI, hrdashboard", "dev002, CDPLIFE, hrdashboard", "dev002, third, hrdashboard" })

这表示,添加了这两个注解的方法会执行三次,每次会传入三个参数

@RepeatedTest(n):重复性测试,即执行n次

6、@ValueSource、@EnumSource、@MethodSource、@CsvSource、@CsvFileSource、@ArgumentsSource

这些注解都是和@ParameterizedTest组合使用,用来指定参数源的。

// 执行三次方法,类型还支持float、String、long等类型
@ValueSource(ints = { 1, 2, 3 })
// @EnumSource提供了使用Enum常量的方便方法。该注释提供了一个可选的名称参数,允许您指定哪个常量应使用。如果省略,所有的常量将被使用,如下面的示例所示。
@EnumSource(TimeUnit.class)
// @MethodSource 允许引用测试类或外部类的一个或多个方法。此类方法必须返回流、可迭代、迭代器或参数数组。此外,这种方法不能接受任何参数。测试类中的工厂方法必须是静态的,除非用@TestInstance(Lifecycle.PER_CLASS)注释测试类;
@MethodSource("roleAndMenuParam")
void testSavePlatformRoleAndAuthMenu(PlatformAdminAuthMenuForm platformAdminAuthMenuForm) { 
}
static List<PlatformAdminAuthMenuForm> roleAndMenuParam() {    
}
// 参数以逗号分隔,以此为例,会执行三次这个测试方法,传入三个参数
@CsvSource({ "dev002, SP-CI, hrdashboard", "dev002, CDPLIFE, hrdashboard", "dev002, third, hrdashboard" })
// @CsvFileSource允许使用类路径中的CSV文件。CSV文件中的每一行都会调用一次参数化测试。
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)

@ArgumentsSource 可用于指定自定义的、可重用的ArgumentsProvider。

7、@Disabled

添加了该注解的方法运行的时候不会执行

四、断言

断言在每个测试方法建议最少有一个

Junit5的断言支持Lambda表达式

以下列举部分常见断言:

  • assertEquals:断言预期和实际是相等的
  • assertNotNull:结果不是null
  • assertThrows:校验异常,如果没有抛出异常或者抛出的异常与预期异常不一致,会断言失败
  • assertAll:分组断言,可以由多个断言组合而成,只要其中有一个不通过就不会继续执行

更多断言不一一列举

五、模拟方法返回值

在写单元测试的时候,有时会需要让方法返回自己想要的结果,这个时就需要使用mock来模拟方法的返回值

mockito实现模拟的原理是通过反射创建一个继承了原类的匿名子类,使用创建的子类代替原来的类执行,由于是继承,所以静态方法、final方法、私有方法无法通过子类进行重写

模拟返回值
// 原方法
appRoleMapper.selectByExample(AppRoleExample example);
// 模拟返回值
when(appRoleMapper.selectByExample(any())).thenReturn(null);

这个方法的作用是指在后面的代码中如果执行到appRoleMapper.selectByExample(AppRoleExample example)方法,参数为任意类型参数,都不会去调用数据库,会直接返回null

doNothing

有些方法没有返回值,如发送消息等方法,如果不需要调用这些方法,可以使用doNothing方法,这样遇到这个方法就不会去执行这个方法,示例如下:

// 原方法
commonService.verifyFieldRequired(Object obj, String jsonPaths){}
// doNothing使用
Mockito.doNothing().when(commonService).verifyFieldRequired(Mockito.any(), Mockito.anyString());
Mockito参数

在模拟方法的时候,需要参数,可以指定any()或anyString()等来指定参数类型,any表示任意类型对象,anyString()表示任意String类型参数,其他类型:

any(Class type):可以传具体的类进来,如果any(AppRole.class)代表任意的角色对象

anyBoolean():任意boolean类型数据

anyListOf(Class clazz):代表某个类型的列表

anyObject():任意对象

更多方法不一一列举

创建模拟对象

测试数据有时候需要的比较多,一个一个创建比较繁琐,可以使用EasyRandom创建随机对象

EasyRandom easyRandom = new EasyRandom();
// 生成单个对象
AppMenu menu = easyRandom.nextObject(AppMenu.class);
// 生成列表
List<AppMenu> menuList = easyRandom.objects(AppMenu.class, 10).collect(Collectors.toList());
// 生成其他类型数据
boolean boo = easyRandom.nextBoolean();

如果不设置参数,随机生成的数据比较乱,可以加一个配置,使得生成的数据更符合我们的要求,使用方法如下

EasyRandomParameters param = new EasyRandomParameters();
// 设置字符串长度范围
param.setStringLengthRange(new Range(5,10));
// 创建对象的时候将配置参数传入
EasyRandom easyRandom = new EasyRandom(param);
for (int i = 0; i < 5; i++) {
    String str = easyRandom.nextObject(String.class);
    System.out.println(str);
}
// 结果
tniOAot
nzWvk
QtWruzGVy
IdgFUY
NdkFYkk

更多配置请查看官方文档:https://github.com/j-easy/easy-random/wiki/Randomization-parameters

模拟静态方法、私有方法返回值

前面有说到mockito是通过反射创建原类的子类来实现的模拟,这样的方法无法模拟静态方法、私有方法、final方法,此时可以使用JMockit来实现

JMockit中文网:http://jmockit.cn/index.htm

JMock使用方法一:

class LogTestServiceTest {

    @Autowired
    private static LogTestService logTestService = new LogTestService();

    @Test
    void testTest() throws Exception {
        new Expectations(DataUtils.class, LogTestService.class) {
            {
                // mock静态方法
                DataUtils.isEmptyList(Mockito.anyList());
                result = false;
                // mock final方法
                logTestService.finalTest(Mockito.anyString());
                result = "mock final";
                // 私有方法与native方法不可以通过这个方式mock
            }
        };
        logTestService.test();
    }
}

JMock使用方法二:

class LogTestServiceTest {

    @Autowired
    private static LogTestService logTestService = new LogTestService();
    
    // 写一个内部类,继承MockUp
    public static class LogTestServiceTestMockUp extends MockUp<LogTestService> {
        @Mock
        private String privateTest(String str) {
            return "mock private method";
        }
        @Mock
        final String finalTest(String str) {
            return "mock final method";
        }
    }
    
    public static class DataUtilsMockUp extends MockUp<DataUtils> {
        @Mock
        public static boolean isEmptyList(List list) {
            return false;
        }
    }

    @Test
    void testTest() throws Exception {
        new LogTestServiceTestMockUp();
        new DataUtilsMockUp();
        logTestService.test();
    }
}

六、项目依赖

<!-- mockito -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.11.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
    <scope>test</scope>
</dependency>
<!-- junit5 -->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-commons</artifactId>
    <version>1.8.2</version><!--$NO-MVN-MAN-VER$-->
</dependency>
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.8.2</version><!--$NO-MVN-MAN-VER$-->
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-commons</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- JMock -->
<dependency>
    <groupId>org.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.36</version>
    <scope>test</scope>
</dependency>