基于属性的Java测试:一个测试替代一千个
\n\n您是否曾经编写过一个单元测试,它通过了绿色,但一周后却在生产环境中因为“某个奇怪的值”而失败?很熟悉吧。常规的(基于示例的)测试就像检查水壶是否沸腾,但只在100°C时检查。如果气压不同呢?如果海拔高度不同呢?基于属性的Java测试正是为了解决这个问题:您不需要手动列举示例,而是描述您的代码必须满足的属性,而框架会生成数百个随机输入数据并检查一切是否正常。
\n\n这种方法对于编写复杂算法、处理集合、解析器或数学函数的Java开发者尤其有用。在本文中,我们将探讨如何使用jqwik库将基于属性的测试集成到您的项目中,并展示真实的代码示例。
\n\n什么是基于属性的测试,它与常规测试有何不同?
\n\n在经典方法中,您编写:
\n\n@Test\nvoid testAddition() {\n assertEquals(4, Calculator.add(2, 2));\n assertEquals(0, Calculator.add(-1, 1));\n}\n\n一切正常,直到有人传入了Integer.MAX_VALUE。或者null。或者一个非常大的负数。基于属性的测试颠覆了逻辑:您说的不是“期望某个结果”,而是“对于任何有效的输入,结果都满足属性X”。
例如,加法的属性:add(a, b) == add(b, a)(交换律)。或者add(a, 0) == a(单位元)。框架会生成数千个随机的a和b对,并检查属性是否被违反。
基于属性的测试的主要优势:
\n- \n
- 自动生成测试数据 — 您会发现甚至没想过的Bug。 \n
- 减少测试数量 — 一个属性测试可以替代数十个基于示例的测试。 \n
- 检查边界情况 — 生成器能够创建“危险”的值(0、null、空字符串、大数字)。 \n
- Shrinking — 如果测试失败,框架会将输入数据最小化到能重现错误的最简单示例。 \n
Java中用于基于属性测试的库
\n\n市场上有几个成熟的解决方案:
\n- \n
- jqwik — 现代、轻量级,与JUnit 5完美集成。推荐作为入门选择。 \n
- QuickTheories — 速度快,但不太流行。 \n
- JUnit QuickCheck — 旧Scala工具的分支,但支持不佳。 \n
我们将使用jqwik,因为它拥有最方便的API和出色的文档。在pom.xml中添加依赖:
<dependency>\n <groupId>net.jqwik</groupId>\n <artifactId>jqwik</artifactId>\n <version>1.8.4</version>\n <scope>test</scope>\n</dependency>\n\n或者对于Gradle:
\n\ntestImplementation 'net.jqwik:jqwik:1.8.4'\n\n编写第一个基于属性的Java测试
\n\n假设我们有一个反转字符串的方法:
\n\npublic static String reverse(String s) {\n if (s == null) return null;\n return new StringBuilder(s).reverse().toString();\n}\n\n可以检查什么属性?如果反转字符串两次,我们会得到原始字符串。这是基于属性的测试的理想属性。
\n\nimport net.jqwik.api.*;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass StringProperties {\n