一、背景
单测定义
是开发提测前研发自测的一种手段和方式,用一段代码检测业务功能是否按照需求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 {
@BeforeAllpublic static void beforeClass() {
System.out.println("-------before class execute-------");}@BeforeEachpublic 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));}@AfterAllpublic static void afterClass() {
System.out.println("-------after class execute-------");}@AfterEachpublic void afterTest() {
System.out.println("》》》》》》》after Test execute《《《《《《《");}
}
依赖注入
@DisplayName("依赖注入测试")
public class DITest {
DITest(TestInfo testInfo) {
System.out.println("实例化的时候注入一次");Assertions.assertEquals("依赖注入测试", testInfo.getDisplayName());}@BeforeEachpublic 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"));}@Testvoid test2() {
}@DisplayName("testReporter 1")@Testvoid reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");}@DisplayName("testReporter 2")@Testvoid 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 {
@BeforeAllpublic static void beforeClass() {
System.out.println("-------before class execute-------");}@BeforeEachpublic void beforeTest() {
System.out.println("》》》》》》》before Test execute《《《《《《《");}@Nestedclass 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));}}@AfterAllpublic static void afterClass() {
System.out.println("-------after class execute-------");}@AfterEachpublic 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 {
@Overridepublic Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("lw", "bj").map(Arguments::of);}}
}
动态测试
@DisplayName("动态测试")
public class DynamicTestsDemo {
@TestFactoryCollection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(dynamicTest("1st dynamic test", Assertions::fail),dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2)));}@TestFactoryIterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(dynamicTest("3rd dynamic test", () -> assertTrue(true)),dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2)));}@TestFactoryIterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(dynamicTest("5th dynamic test", () -> assertTrue(true)),dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))).iterator();}@TestFactoryStream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("A", "B", "C").map(str -> dynamicTest("test" + str, () -> assertTrue(true)));}@TestFactoryStream<DynamicTest> dynamicTestsFromIntStream() {
return IntStream.iterate(0, n -> n + 2).limit(10).mapToObj(n -> dynamicTest("test" + n, () -> assertEquals(n % 2, 0)));}@TestFactoryStream<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;@Overridepublic boolean hasNext() {
current = random.nextInt(100);return current % 7 != 0;}@Overridepublic 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);}@TestFactoryStream<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);}
持续更新中…