天天看点

七爪源码:在 Scala 中使用生成器进行测试驱动开发

作者:庄志炎

使用虚假和随机数据进行测试

七爪源码:在 Scala 中使用生成器进行测试驱动开发

使用随机假数据进行测试

我们在编写单元测试时都需要测试数据。

传统方法 - 我们自己生成模型。 例如,以下代码包含测试数据产品。

it must "return total cost of a product when the delivery cost is ignored" in {
  val product = Product(id=1, name="test-product", unitPrice=10, deliveryCost=1)

  dao.saveProduct(product)

  val result = dao.calculateCost(product.id, includeDeliveryCost = false)

  result must be(product.unitPrice)
}           

这种方法有什么问题?

  • 耗时(如果模型很大)。
  • 测试主体可能很长(如果模型很大),并且它们主要由测试数据占据。
  • 在我们自己生成测试数据时,我们可能会错过一些极端情况(本故事稍后会介绍这一点)。

另一种方法是使用生成器 [1] 生成虚假和随机测试数据,如下所示。

it must "return total cost of a product when the delivery cost is ignored" in {
  // generate a random product
  val product = genProduct()

  dao.saveProduct(product)

  val resultFromDB = dao.calculateCost(product.id, includeDeliveryCost = false)

  resultFromDB must be(product.unitPrice)
}           

我为什么要那么做?

  • 每次运行测试时都会有一个新的输入。
  • 易于生成测试数据。
  • 测试主体是干净的(测试数据的代码更少)。
  • 生成器驱动的测试有时会捕获我们不知道的错误。 例如,我能够捕捉到数据库拒绝存储 Unicode 字符的错误。

使您的预期结果明确而详细

如前所述,生成器驱动测试 [1] 帮助我们生成测试数据。

但是当预期结果很复杂时,它有时会增加单元测试的复杂性。

比如之前的单元测试,DB返回的结果,预期的结果简单易懂。

resultFromDB 必须是(product.unitPrice)

但是,在某些情况下,预期结果很复杂,例如学生的进度报告。

case class ProgressReport(
  minScore: Int,
  maxScore: Int
)
val myTestData = ???
// 这里我们试图从我们的
// 伪造的测试数据。
// 它添加:
// 1. 不必要的复杂性。
// 2. 难以推理
// 3. 不明确
val expectedProgressReport: ProgressReport = myFakeData.map( ... )
reportFromDB must be(expectedProgressReport)           
因此,无论生成器如何,都最好使您的预期结果明确而详细。

让我们通过一个例子来理解这一点。

想象一下,我们想添加一个功能来计算学生的进度报告。

def calculateProgress(
  studentId: Long
): ProgressReport = ???           

首先,我们使用生成器 [1] 生成模型数据,并仅覆盖那些可能影响我们最终结果的属性。

val student = genStudent()
val course = genCourse()           

// 因为它是关于计算分数的,所以我们只修改了 // score 属性。 这样,很容易推理。

val studentScore = genStudentScore(course.id, student.id).copy(score = 10)

然后,我们明确地写出预期的结果。

val expectedResult = ProgressReport(minScore = 10, maxScore = 10)           

因此,我们的单元测试如下所示。

it should "calculate progress report of a student" in {
  val student = genStudent()
  val courseA = genCourse()
  val courseB = genCourse()
  
  // we are only modifying score attribute
  val studentScoreForCourseA = genStudentScore(courseA.id,   student.id).copy(score = 80)
  val studentScoreForCourseB = genStudentScore(courseB.id, student.id).copy(score = 90)
 
  // Writing expected results explicitly. This way, it's easy to
  // read and reason about.
  val expectedResult = ProgressReport(minScore = 80, maxScore = 90)
  
  // Add test data to our system here 
  // i.e student record, his courses, and score in each course.
  // Retrieve progress from DB.   
  val progressFromDB = dao.calculateProgress(student)
  progressFromDB must be(expectedResult)
}           

谢谢阅读。

如果您有任何问题,请随时提问。 我很乐意回答。

关注七爪网,获取更多APP/小程序/网站源码资源!