Kotlin中的基于属性的测试:比你想象的更快地破坏代码

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

为什么普通测试会撒谎,而你还相信它们



想象一下:你写了一个函数,用三个单元测试覆盖了它。绿色勾号。CI通过。生产环境——出错了。熟悉吗?



经典单元测试的问题在于它们只检查特定的例子。你自己想出了输入数据,又自己编写了预期结果。这就像一场考试,学生自己出题。迟早你会遗漏一个边界情况——null、空列表、负数或包含Unicode字符的字符串。



这时,基于属性的测试(property-based testing)登场了。与其编写“如果传入5,应该返回25”,你不如制定一个属性:“对于任何整数,其平方总是非负的”。然后框架会自动生成数百个随机值并检查这个属性。



在本文中,我们将探讨如何在Kotlin中使用Kotest库进行基于属性的测试。你将学习如何编写可靠的测试、在早期阶段发现错误,以及不再害怕重构。这不仅仅是枯燥的理论——我们将编写可以直接应用于项目的真实代码。



什么是基于属性的测试,为什么选择Kotlin?



这个想法并不新鲜——早在1999年,Haskell的QuickCheck库就推广了它。但直到现在,随着后端系统和微服务复杂性的增加,基于属性的测试才成为开发者技术栈中的必备工具。



Kotlin是实现这种方法的理想语言。为什么?



  • 函数式特性:Lambda表达式、扩展函数、不可变性——所有这些都简化了纯属性的编写。
  • Kotest是一个强大的框架,原生支持基于属性的测试、生成器和Shrinking(最小化失败示例的算法)。
  • 与Java的兼容性:你可以使用相同的方法测试用Java编写的遗留代码。


让我们直接进入正题。下面是一个将颠覆你对测试认知的例子。



5分钟内完成第一个基于属性的测试



假设我们有一个检查字符串是否为回文的函数:



fun isPalindrome(s: String): Boolean {    val cleaned = s.filter { it.isLetterOrDigit() }.lowercase()    return cleaned == cleaned.reversed()}


普通测试看起来像这样:



@Testfun `test known palindromes`() {    assertTrue(isPalindrome("A man, a plan, a canal: Panama"))    assertTrue(isPalindrome("racecar"))    assertFalse(isPalindrome("hello"))}


一切正常。但是,如果输入数据包含空格、数字或符号呢?如果字符串是空的呢?如果只包含空格呢?



现在,让我们使用Kotest编写一个基于属性的测试:



import io.kotest.core.spec.style.StringSpecimport io.kotest.property.forAllimport io.kotest.property.arbitrary.string

class PalindromePropertyTest : StringSpec({ "任何经过清理的字符串,其自身应该是回文" { forAll<String> { original -> val cleaned = original.filter { it.isLetterOrDigit() }.lowercase() isPalindrome(cleaned) == true } }})


这里发生了什么?forAll<String>生成随机字符串:空字符串、带空格的、带Unicode的、带数字的。然后对每个字符串检查属性。如果任何一个字符串违反了断言,测试就会失败,并且Kotest会显示最小的可重现示例。



在这个阶段,你已经可以发现错误。例如,如果字符串只包含空格——filter会返回一个空字符串,而isPalindrome("")会返回true。普通测试可能会漏掉这个情况。



生成器:你

博客

书籍推荐