☰
Current Page
Main Menu
Home
Home
Editing
JUnit5单元测试
Edit
Preview
h1
h2
h3
default
Set your preferred keybinding
default
vim
emacs
markdown
Set this page's format to
AsciiDoc
Creole
Markdown
MediaWiki
Org-mode
Plain Text
RDoc
Textile
Rendering unavailable for
BibTeX
Pod
reStructuredText
Help 1
Help 1
Help 1
Help 2
Help 3
Help 4
Help 5
Help 6
Help 7
Help 8
Autosaved text is available. Click the button to restore it.
Restore Text
### JUnit JUnit是一个开源的Java语言的单元测试框架,专门针对Java设计,使用最广泛。JUnit是事实上的单元测试的标准框架,任何Java开发者都应当学习并使用JUnit编写单元测试。 使用JUnit编写单元测试的好处在于,我们可以非常简单地组织测试代码,并随时运行它们,JUnit就会给出成功的测试和失败的测试,还可以生成测试报告,不仅包含测试的成功率,还可以统计测试的代码覆盖率,即被测试的代码本身有多少经过了测试。对于高质量的代码来说,测试覆盖率应该在80%以上。 此外,几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写并运行JUnit测试。JUnit目前最新版本是5,也是这里要介绍的JUnit版本。 使用JUnit很方便,选中需要单测的方法,点击右键 Generate -> Tests 弹出下图JUnit5来创建测试代码。  什么是单元测试呢?单元测试是针对最小的功能单元编写测试代码。Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。 单元测试有什么好处呢?在学习单元测试前,我们可以先了解一下测试驱动开发。 所谓测试驱动开发,是指先编写接口,紧接着编写测试。编写完测试后,我们才开始真正编写实现代码。在编写实现代码的过程中,一边写,一边测,什么时候测试全部通过了,那就表示编写的实现完成了: 编写接口 │ ▼ 编写测试 │ ▼ ┌─> 编写实现 │ │ │ N ▼ └── 运行测试 │ Y ▼ 任务完成 这就是我们通常所说的TDD。当然,这是一种理想情况。大部分情况是我们已经编写了实现代码,需要对已有的代码进行测试。 单元测试可以确保单个方法按照正确预期运行,如果修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动正确。此外,测试代码本身就可以作为示例代码,用来演示如何调用该方法。 使用JUnit进行单元测试,我们可以使用断言(Assertion)来测试期望结果,可以方便地组织和运行测试,并方便地查看测试结果。此外,JUnit既可以直接在IDE中运行,也可以方便地集成到Maven这些自动化工具中运行。 在编写单元测试的时候,我们要遵循一定的规范: - 单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试,这样套娃的做法; - 每个单元测试应当互相独立,不依赖运行的顺序; - 测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为0,null,空字符串""等情况。 不要对单元测试存在如下误解: - 那是测试同学干的事情,实际上单元测试与开发同学强相关的; - 单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的; - 单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态; - 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。 ### 编写JUnit测试 我们编写了一个计算阶乘的类,它只有一个静态方法来计算阶乘: ``` n! = 1 × 2 × 3 × ...× n ``` 代码如下: ```java public class Factorial { public static long fact(long n) { if (n < 0) { throw new IllegalArgumentException(); } long r = 1; for (long i = 1; i <= n; i++) { r = r * i; } return r; } } ``` 然后按照上述的方式点击右键,生成对应的测试类 FactorialTest.java : ```java public class FactorialTest { @Test void testFact() { assertEquals(1, Factorial.fact(1)); assertEquals(2, Factorial.fact(2)); assertEquals(6, Factorial.fact(3)); assertEquals(3628800, Factorial.fact(10)); assertEquals(2432902008176640000L, Factorial.fact(20)); } @Disabled @Test void testNegative() { assertThrows(IllegalArgumentException.class, () -> { Factorial.fact(-1); }); } } ``` 执行这个单元测试,在testFact中,我们给出了入参和期待的值,并断言是否成功。 除了常规的测试用例以外,可以发现上面的例子对异常情况也有测试,testNegative. 在Java程序中,异常处理是非常重要的。我们自己编写的方法,也经常抛出各种异常。对于可能抛出的异常进行测试,本身就是测试的重要环节。因此,在编写JUnit测试的时候,除了正常的输入输出,我们还要特别针对可能导致异常的情况进行测试。 我个人对于程序的理解除了完成正常的逻辑外,绝大部分时间都是在处理异常,逻辑的复杂性绝大部分都是异常带来的, 正常的逻辑可以梳理清楚,但是异常太多太多了,程序在执行过程中有可能随时产生异常,比如io异常、参数错误、超时等等,各种情况下都有可能产生异常。 我个人觉得异常应该作为一种程序结构集成在我们的代码中,因为这是客观存在的,我不太喜欢那种大统一的无差别try然后catch打印日志。 我更倾向于在执行逻辑过程中随时随地的判断异常,当然带来的坏处是编写过程很繁琐,但我个人觉得无非是多打几个字而已,换来的是心里的踏实和程序的健壮。 回到异常情况下的单元测试,JUnit提供了assertThrows()来期望捕获一个指定的异常。第二个参数Executable封装了我们要执行的会产生异常的代码。当我们执行Factorial.fact(-1)时,必定抛出IllegalArgumentException。assertThrows()在捕获到指定异常时表示通过测试,未捕获到异常,或者捕获到的异常类型不对,均表示测试失败。 经过思考,观察Factorial.fact()方法,注意到由于long型整数有范围限制,当我们传入参数21时,得到的结果是-4249290049419214848,而不是期望的51090942171709440000,因此,当传入参数大于20时,Factorial.fact()方法应当抛出ArithmeticException。而且已有的单元测试用例已经覆盖了所有的代码行和分支。 这只是一个简单的求阶乘的算法,就有很多异常情况,更何况复杂的算法和业务逻辑,所以说写一个健壮的程序是多么不容易啊,但我们还是需要向这个目标前进,不断地完善我们单元测试用例, 至少能保证做到,凡是通过我们编写的单元测试,至少保证目前为止我们认为的健壮性,如果后续发现有问题,就继续补充单元测试,完善功能。 ### jacoco 单元覆盖率测试 JUnit5是Java编程语言的单元测试框架。覆盖率是指代码被测试覆盖的程度,即代码中被测试覆盖的语句、分支、条件等的比例。JUnit5可以通过集成代码覆盖率工具来测量代码覆盖率。 如果已经编写了完善的单元测试用例,那么使用jacoco进行代码覆盖率统计就很容易了。 在pom.xml中加入jacoco插件 ```xml <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.6</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> <configuration> <!--定义输出的文件夹--> <outputDirectory>target/jacoco-report</outputDirectory> <!--执行数据的文件--> <dataFile>${project.build.directory}/jacoco.exec</dataFile> <!--要从报告中排除的类文件列表,支持通配符(*和?)。如果未指定则不会排除任何内容--> <!-- <excludes>**/test/*.class</excludes>--> <!-- https://www.baeldung.com/jacoco-report-exclude--> <excludes> <exclude>com/example/demo/*</exclude> </excludes> <!--包含生成报告的文件列表,支持通配符(*和?)。如果未指定则包含所有内容--> <includes> <include>com/example/demo2/*</include> </includes> <!--HTML 报告页面中使用的页脚文本。--> <footer></footer> <!--生成报告的文件类型,HTML(默认)、XML、CSV--> <!-- <formats>HTML</formats>--> <!--生成报告的编码格式,默认UTF-8--> <outputEncoding>UTF-8</outputEncoding> <!--抑制执行的标签--> <skip></skip> <!--源文件编码--> <sourceEncoding>UTF-8</sourceEncoding> <!--HTML报告的标题--> <title>${project.name}</title> </configuration> </execution> </executions> </plugin> ``` 在执行 mvn test 后在target/jacoco-report目录中自动生成 index.html 等页面相关文件。  点击 index.html 在浏览器中展示,即可看到具体的单测结果的覆盖率。  由上图可知,指令覆盖率是85%,分支覆盖率是75% 继续查看具体的覆盖情况  标红的代码行是单元测试没有覆盖的,标绿说明单元测试已经覆盖。 我们继续增加单元测试 ```java @Test void testNegative() { assertThrows(IllegalArgumentException.class, () -> { Factorial.fact(-1); }); } ``` 重新生成测试覆盖统计:  可见已经达到了100%覆盖。 ### 引擎例子 - 启动引擎 ```java public class SieEngineTestExtension implements BeforeEachCallback, Extension { private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create("com.sie.engine"); @Override public void beforeEach(ExtensionContext context) throws Exception { startTestEngine(context); } public static void startTestEngine(ExtensionContext context) { // 启动容器的逻辑 TestEngineStart.start(); storeTestEngineStarted(context); } public static void storeTestEngineStarted(ExtensionContext context) { context.getStore(NAMESPACE).put("testEngineStarted", true); } public static boolean isTestEngineStarted(ExtensionContext context) { return context.getStore(NAMESPACE).get("testEngineStarted", Boolean.class); } } ``` 自定义一个 SieEngineTestExtension 类,该类会启动引擎,初始化测试环境。 - 使用 ```java @ExtendWith(SieEngineTestExtension.class) // 执行测试前先启动引擎 class RecordSetTest { @Test void call() { Meta meta = BaseContextHandler.getMeta(); System.out.println("userId = " + meta.getUserId()); System.out.println("tenantId = " + meta.getTenantId()); assertNotNull(meta); assertNotNull(meta.get("rbac_user")); Object object = meta.get("rbac_user").search(new Filter(), null, null, null, null); System.out.println(object); System.out.println("===========测试完成==========="); } } ``` 使用 @ExtendWith(SieEngineTestExtension.class) 注解来标识该测试类启动前会先启动引擎,那么就可以在引擎中进行后续的测试。 还是在上面的代码示例,就可以测试meta相应的方法,比如 meta.get("rbac_user").search(new Filter(), null, null, null, null); 执行mvn test 生成jacoco覆盖率图,比如下图:  ### 单元测试规范 1. 【强制】好的单元测试必须遵守AIR原则。 <br><span style="color:orange">说明</span>:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。 - A:Automatic(自动化) - I:Independent(独立性) - R:Repeatable(可重复) 2. 【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。 3. 【强制】保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。 <br><span style="color:red">反例</span>:method2需要依赖method1的执行,将执行结果作为method2的输入。 4. 【强制】单元测试是可以重复执行的,不能受到外界环境的影响。 <br><span style="color:orange">说明</span>:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。 <br><span style="color:green">正例</span>:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring 这样的DI框架注入一个本地(内存)实现或者Mock实现。 5. 【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。 <br><span style="color:orange">说明</span>:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。 6. 【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。 <br><span style="color:orange">说明</span>:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。 7. 【强制】单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。 <br><span style="color:orange">说明</span>:源码构建时会跳过此目录,而单元测试框架默认是扫描此目录。 8. 【推荐】单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100% <br><span style="color:orange">说明</span>:在工程规约的应用分层中提到的DAO层,Manager层,可重用度高的Service,都应该进行单元测试。 9. 【推荐】编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。 - B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。 - C:Correct,正确的输入,并得到预期的结果。 - D:Design,与设计文档相结合,来编写单元测试。 - E:Error,强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),并得到预期的结果。 10. 【推荐】对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。 <br><span style="color:red">反例</span>:删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数据并不符合业务插入规则,导致测试结果异常。 11. 【推荐】和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识。 <br><span style="color:green">正例</span>:在RDC内部单元测试中,使用RDC_UNIT_TEST_的前缀标识数据。 12. 【推荐】对于不可测的代码建议做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码。 13. 【推荐】在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例(UC)。 14. 【推荐】单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,建议在项目提测前完成单元测试。 15. 【参考】为了更方便地进行单元测试,业务代码应避免以下情况: - 构造方法中做的事情过多。 - 存在过多的全局变量和静态方法。 - 存在过多的外部依赖。 - 存在过多的条件语句。 <br><span style="color:orange">说明</span>:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。 16. 【参考】不要对单元测试存在如下误解: - 那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。 - 单元测试代码是多余的。汽车的整体功能与各单元部件的测试正常与否是强相关的。 - 单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。 - 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。 ### cicd
Uploading file...
Sidebar
[[_TOC_]]
Edit message:
Cancel