一、背景
单测定义
是开发提测前研发自测的一种手段和方式,用一段代码检测业务功能是否按照需求case执行
为什么要单测
起源
在敏捷开发中的有一项核心实践和技术设计方法论,TDD(测试驱动开发)
TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码
好处
保障代码最有效的办法(还有CR)
降本增效,高效协作,减少BUG量
目的
验证行为(是否如期执行,异常是否处理)
为设计铺垫(提前思考系统的设计)
自动化回归(快速简单的运行)
单测规范
AIR(单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点)
规范 | 说明 |
---|---|
A(Automatic自动化 ) | 全程自动化 无需人肉验证 断言 |
(Independent独立性) | 单元测试之前独立运行 不依赖 无先后顺序 |
R(Repeatable可重复) | 可重复执行不受环境影响 |
注意事项:质量、实施方案、运行时长
二、Testing框架
TestNg 借鉴于Junit但功能更加强大的测试框架(集成测试、依赖测试)
Junit 主流单测框架
部分功能对比
注解差异
三、Junit5
Junit5整体结构
添加Maven坐标
<!-- Junit5-->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
<!-- Junit5-->
简单测试
@DisplayName("简单测试")
public class SimpleTest {
@BeforeAll
public static void beforeClass() {
System.out.println("-------before class execute-------");
}
@BeforeEach
public void beforeTest() {
System.out.println("》》》》》》》before Test execute《《《《《《《");
}
@Test
@DisplayName("测试加法")
public void testSum(){
System.out.println("测试加法");
Assertions.assertEquals(2, Calculator.sum(1,1));
}
@Test
@DisplayName("测试减法")
public void testSub(){
System.out.println("测试减法");
Assertions.assertEquals(0, Calculator.sub(1,1));
}
@Test
@DisplayName("测试乘法")
public void testMul(){
System.out.println("测试乘法");
Assertions.assertEquals(1, Calculator.mul(1,1));
}
@Test
@DisplayName("测试除法")
public void testDiv(){
System.out.println("测试除法");
Assertions.assertEquals(1, Calculator.div(1,1));
}
@AfterAll
public static void afterClass() {
System.out.println("-------after class execute-------");
}
@AfterEach
public void afterTest() {
System.out.println("》》》》》》》after Test execute《《《《《《《");
}
}
依赖注入
@DisplayName("依赖注入测试")
public class DITest {
DITest(TestInfo testInfo) {
System.out.println("实例化的时候注入一次");
Assertions.assertEquals("依赖注入测试", testInfo.getDisplayName());
}
@BeforeEach
public void beforeTest(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
Assertions.assertEquals("TEST 1", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
void test2() {}
@DisplayName("testReporter 1")
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@DisplayName("testReporter 2")
@Test
void reportSeveralValues(TestReporter testReporter) {
HashMap<String, String> values = new HashMap<>();
values.put("name", "july");
values.put("year", "1993");
testReporter.publishEntry(values);
}
}
断言测试
@DisplayName("断言测试")
public class AssertTest {
@Test
@DisplayName("测试断言equals")
void testEquals() {
assertTrue(3 < 4);
}
@Test
@DisplayName("测试断言NotNull")
void testNotNull() {
assertNotNull(new Object());
}
@Test
@DisplayName("测试断言抛异常")
void testThrows() {
ArithmeticException arithExcep = assertThrows(ArithmeticException.class, () -> {
int m = 5/0;
});
assertEquals("/ by zero", arithExcep.getMessage());
}
@Test
@DisplayName("测试断言超时")
void testTimeOut() {
String actualResult = assertTimeout(ofSeconds(2), () -> {
Thread.sleep(1000);
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
@DisplayName("测试组合断言")
void testAll() {
assertAll("测试商城下单",
() -> assertTrue(2 < 2, "库存不足"),
() -> assertTrue(1 < 2,"余额不足"),
() -> assertNotNull(new Object(),"交易异常")
);
}
}
嵌套测试
@DisplayName("嵌套测试")
public class NestedTest {
@BeforeAll
public static void beforeClass() {
System.out.println("-------before class execute-------");
}
@BeforeEach
public void beforeTest() {
System.out.println("》》》》》》》before Test execute《《《《《《《");
}
@Nested
class CalculateDemo {
@AfterEach
@DisplayName("afterCalculateDemo")
public void afterTest() {
System.out.println("afterCalculateDemo........");
}
@Test
@DisplayName("testCalculateDemo")
public void testCalculateDemo(){
System.out.println("testCalculateDemo........");
Assertions.assertEquals(6, Calculator.sum(3, 3));
}
}
@AfterAll
public static void afterClass() {
System.out.println("-------after class execute-------");
}
@AfterEach
public void afterTest() {
System.out.println("》》》》》》》after Test execute《《《《《《《");
}
}
超时测试
@DisplayName("执行超时验证")
public class TimeOutTest {
@Test
@DisplayName("执行超时失败验证")
void testFailWithTimeout() throws InterruptedException {
Assertions.assertTimeout(Duration.ofMillis(100), () -> Thread.sleep(100));
}
@Test
@DisplayName("执行超时成功验证")
void testSuccessWithTimeout() throws InterruptedException {
Assertions.assertTimeout(Duration.ofMillis(100), () -> Thread.sleep(99));
}
}
异常测试
@DisplayName("异常测试")
public class ExceptionTest {
@Test
@DisplayName("测试抛出RunTimeException")
void testThrowsException2(){
Assertions.assertThrows(RuntimeException.class, () -> {
throw new RuntimeException();
});
}
}
参数化测试
@DisplayName("参数化测试")
public class ParameterizedTestDemo {
@DisplayName("测试ValueSource参数One")
@ParameterizedTest
@ValueSource(strings = {"111", "222", "I am july"})
public void testWithValueSourceOne(String candidate) {
System.out.println(candidate);
Assertions.assertTrue(StringUtils.isNumeric(candidate));
}
@DisplayName("测试ValueSource参数Two")
@ParameterizedTest
@ValueSource(ints = {111,222,333})
public void testWithValueSourceTwo(int argument) {
System.out.println(argument);
Assertions.assertEquals(argument % 111, 0);
}
/**
* @ EnumSource能够很方便地提供Enum常量。该注解提供了一个可选的names参数,
* 你可以用它来指定使用哪些常量。如果省略了,就意味着所有的常量将被使用
*/
@DisplayName("测试ValueSource枚举参数All")
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSourceEnumAll(TimeUnit timeUnit) {
System.out.println(timeUnit.name());
Assertions.assertNotNull(timeUnit);
}
@DisplayName("测试ValueSource枚举参数Part")
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"})
void testWithEnumSourceEnumPart(TimeUnit timeUnit) {
System.out.println(timeUnit.name());
Assertions.assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}
/**
* @ EnumSource注解还提供了一个可选的mode参数,它能够细粒度地控制哪些常量将会被传递到测试方法中。例如,
* 你可以从枚举常量池中排除一些名称或者指定正则表达式,如下面代码所示。
*/
@DisplayName("测试ValueSource枚举参数Exclude")
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EnumSource.Mode.EXCLUDE, names = {"DAYS", "HOURS"})
void testWithEnumSourceExclude(TimeUnit timeUnit) {
assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
Assertions.assertTrue(timeUnit.name().length() > 5);
}
/**
* @ MethodSource允许你引用测试类中的一个或多个工厂方法。这些工厂方法必须返回一个Stream、Iterable、
* Iterator或者参数数组。另外,它们不能接收任何参数。默认情况下,它们必须是static方法,除非测试类使用了
* @ TestInstance(Lifecycle.PER_CLASS)注解。
*/
@DisplayName("测试MethodSource参数One")
@ParameterizedTest
@MethodSource("stringProvider")
void testWithSimpleMethodSourceOne(String argument) {
System.out.println(argument);
Assertions.assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("张三", "李四");
}
@DisplayName("测试MethodSource参数Two")
@ParameterizedTest
@MethodSource("personProvider")
void testWithPersonMethodSourceTwo(Person person) {
System.out.println(person.getName());
Assertions.assertNotNull(person.getName());
}
static Stream<Person> personProvider() {
return Stream.of(JMockData.mock(Person.class), JMockData.mock(Person.class));
}
/**
* 如果测试方法声明了多个参数,则需要返回一个Arguments实例的集合或Stream,
* 如下面代码所示。请注意, Arguments.of(Object ...)是Arguments接口中定义的静态工厂方法。
*/
@DisplayName("测试MethodSource参数Three")
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSourceThree(String str, int num, List<String> list) {
assertEquals(2, str.length());
Assertions.assertTrue(num >= 1 && num <= 2);
assertEquals(3, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(Arguments.of("bj", 1, Arrays.asList("a", "b","c")),
Arguments.of("lw", 2, Arrays.asList("x", "y","z")));
}
/**
* @ CsvSource使用单引号'作为引用字符。请参考上述示例和下表中的'bj,lw'值。一个空的引用值''表示一个空 的String;
* 而一个完全空的值被当成一个null引用。如果null引用的目标类型是基本类型,则会抛出一个ArgumentConversionException。
*/
@DisplayName("测试CsvSource参数One")
@ParameterizedTest
@CsvSource({"bj, 1", "lw, 2", "'bj,lw', 3"})
void testWithCsvSourceOne(String first, int second) {
Assertions.assertNotNull(first);
assertNotEquals(0, second);
}
@DisplayName("测试CsvSource参数Two")
@ParameterizedTest
@CsvSource({"bj,1,11", "lw,2,12", "'bj,lw',3,13"})
void testWithCsvSourceTwo(String first, int second,int three) {
Assertions.assertNotNull(first);
assertNotEquals(0, second);
Assertions.assertTrue(three > 10);
}
/**
* @ CsvFileSource允许你使用类路径中的CSV文件。CSV文件中的每一行都会触发参数化测试的一次调用。
* 与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为引号字符,请参考上面例子中的"bj,
* lw"值,一个空的带引号的值""表示一个空String,一个完全为空的值被当成null引用,如果null引用的目标
* 类型是基本类型,则会抛出一个ArgumentConversionException。
*/
@DisplayName("测试CsvFileSource参数")
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv")
void testWithCsvFileSource(String first, int second) {
Assertions.assertNotNull(first);
assertNotEquals(0, second);
}
/**
* @ ArgumentsSource 可以用来指定一个自定义且能够复用的ArgumentsProvider。
*/
@DisplayName("测试ArgumentsSource参数")
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
Assertions.assertNotNull(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("lw", "bj").map(Arguments::of);
}
}
}
动态测试
@DisplayName("动态测试")
public class DynamicTestsDemo {
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(dynamicTest("1st dynamic test", Assertions::fail),
dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2)));
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(dynamicTest("3rd dynamic test", () -> assertTrue(true)),
dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2)));
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(dynamicTest("5th dynamic test", () -> assertTrue(true)),
dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))).iterator();
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("A", "B", "C").map(str -> dynamicTest("test" + str, () -> assertTrue(true)));
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n, () -> assertEquals(n % 2, 0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTests() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random();
int current;
@Override
public boolean hasNext() {
current = random.nextInt(100);
return current % 7 != 0;
}
@Override
public Integer next() {
return current;
}
};
// Generates display names like: input:5, input:37, input:85, etc.
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
// Executes tests based on the current input value.
ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
.map(input -> dynamicContainer("Container " + input,
Stream.of(dynamicTest("not null", () -> assertNotNull(input)),
dynamicContainer("properties",
Stream.of(dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
)
)
)
));
}
}
测试发现与注册
public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("top.qrainly.share.test.junit.junit5.example.base")
, selectClass(SimpleTest.class), selectClass(DITest.class), selectClass(TimeOutTest.class)
)
.filters(
includeClassNamePatterns(".*Test")
,excludeClassNamePatterns(".TimeOut*")
)
.build();
Launcher launcher = LauncherFactory.create();
TestPlan testPlan = launcher.discover(request);
System.out.println(JSONObject.toJSONString(testPlan));
// 注册执行
TestExecutionListener listener = new SummaryGeneratingListener();
launcher.registerTestExecutionListeners(listener);
launcher.execute(request);
}
持续更新中…