第十一天:用 TDD 实作购物车类别

有了前面的基础,今天我们要在专案里实作一个「购物车(ShoppingCart)」类别。为了确认实作符合预期的规格,我们将会以 TDD(Test-Driven Development)的风格来写程序。换句话说,我们会用「先写测试、再写实际程序」的来回循环来完成这个类别的实作。最终的目标是,当我们把程序码推到 GitHub 时,TeamCity 会自动拉取最新的程序码变更并跑测试。若测试失败的话,我们会收到 TeamCity 的通知并可以在 TeamCity 的 Build Log 里看到所有历程的纪录。

在 2021 年的现代,写测试的方式有千百种,测试框架也是多如牛毛。以 Kotlin 生态系来说,从老牌 的 JUnit 到後起新秀 Kotest 都有各自的风格与专长。刚开始接触 Kotlin 的时候,笔者也是先从 JUnit 开始练习,毕竟 xUnit 系列概念大多是共通的,比较好上手。但随着对 Kotlin 及相关测试工具的认识愈多,发现像 Kotest 这种揉和各家测试风格集大成的框架,其设计更符合 Kotlin 开发者喜欢的简洁风格。在这个系列里,笔者将用 Kotest 测试框架做示范。

在专案里新增 Kotest 测试套件

为了要在练习专案里使用 Kotest,我们要先在专案里新增 Kotest 测试框架 ,这意味着我们要在 Gradle 的 Build Script(也就是在专案根目录底下的 build.gradle.kts)里新增相依套件。

我们可以直接参考 Kotest 官网说明build.gradle.kts 里增加 kotestVersiondependencies 设定:

val kotestVersion: String by project

dependencies {
  // ...
  testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
  testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
  testImplementation("io.kotest:kotest-property:$kotestVersion")
}

其中 $kotestVersion 是我们设定的变数,做为我们统一储存版本号的单一来源。这个变数我们会另外以 Key-Value 的型式存在 gradle.properties 设定档里。至於该用哪个 Kotest 版本?可以到 Maven Central 去查询,以 io.kotest:kotest-runner-junit5 为例,目前最新稳定版是 4.6.2

kotestVersion=4.6.2

设定好 build.gradle.ktsgradle.properties 後,别忘了按一下右上角的 Load Gradle Changes 按钮,IntelliJ IDEA 会重新整理(包括下载、更新、移除)所有的相依,并载入到专案内。

先写一个失败的测试

接着我们来新增我们要建立的类别。首先在 src/main/kotlin 底下新增 Package,对着资料夹按右键,然後选 New > Package 选单,以我自己的 Domain 为例,输入 Package 名称为 io.kraftsman

接着在我们新建的 Package 里新增购物车类别。对着 Package 资料夹按右键,然後选 New > Kotlin Class/File,输入类别名称为 ShoppingCart。IntelliJ IDEA 会在 src/main/kotlin/io.kraftsman 底下建立一个名为 ShoppingCart.kt 的档案,并预先写好 Boilerplate Code 如下:

package io.kraftsman

class ShoppingCart {
}

在 TDD 的最佳实践里,我们要先写一个可以执行、但一定失败的测试,确保我们的测试是真的能被执行,而且如预期的会失败,这样才不会因为所有设定都是 Happy Path 而有偏误。我们可以用 IntelliJ IDEA 来帮我们建立测试类别,把游标放在 ShoppinCart 类别的两个 { } 之间,然後按 ⌘+N 叫出 Generate 选单,选 Test。在弹出视窗里把 Testing library 更换成 Kotest,其他保持预设值後按 OK。IntelliJ IDEA 会自动帮我们产生 ShoppingCartTest 类别在 src/test/io.kraftsman/ 路径底下,并预先写好 Boilerplate Code 如下:

package io.kraftsman

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class ShoppingCartTest : FunSpec({
    
})

接着我们可以直接点选类别名称旁的绿色播放键,让 IntelliJ IDEA 帮我们执行 Kotest 的测试。执行结果会出错是正常的,因为我们本来就打算这样错。

用测试描述购物车的行为

接着我们要开始用写测试的方式来描述我们预期这个 ShoppingCart 类别我们可以怎麽使用?首先我们可以先用一个 context() 函式来描述测试脉络,接着再用 test() 以「测试 3A(Arrange、Act、Assert)」来描述测试的行为。程序码如下:

class ShoppingCartTest : FunSpec({

  context("一个购物车") {
    test("当两个 100 元商品相加时,总价为 200") {
      // Arrange
      val product1 = Product(id = 1, name = "Product 1", price = 100)
      val product2 = Product(id = 1, name = "Product 1", price = 100)
      val shoppingCart = ShoppingCart()

      // Act
      shoppingCart.add(product1)
      shoppingCart.add(product2)

      // Assert
      shoppingCart.totalPrice() shouldBe 200
    }
  }

})

用 IntelliJ IDEA 产生缺漏的程序码

当这段程序码写好时,不用执行就知道编译会失败,因为 IntelliJ IDEA 已经用红字标示出 Product 类别不存在、ShoppingCart 类别上没有 add()totalPrice() 等问题。这时也不用急着自己去修,我们可以让 IntelliJ IDEA 快速地帮我们把决漏的程序码「补」起来。

把游标放在这些被标记成红字的错误程序码上,然後按快速键 Option+Enter(macOS)Alt+Enter(Windows),IntelliJ IDEA 会自动提示该怎麽修正,直接按 Enter 让它帮我们产生 Product 类别及 ShoppingCart 类别的有 add()totalPrice() 方法。

补上逻辑程序码

有 IntelliJ IDEA 帮我们写好基本的程序码「骨架」後,接下来我们只要「填肉」就好。首先打开 Product 类别,由於它只是一个承装商品资讯的容器,在 Kotlin 里可以直接把它宣告成 Data Class。先在 class 前加上 data 关键字,把所有建构子的参数加上 val 宣告,因为这个类别不需要逻辑,所以也把多余的大括号去掉。修改後的程序码长得会像这样:

data class Product(val id: Int, val name: String, val price: Int)

ShoppingCart 类别里,我们在内部参加一个储存 Product 的 List,当使用者呼叫 add() 方法时就可以把 Product 加进 List 里。而 totalPrice 就会回传这个 List 里所有商品 price 的总和。完成後的程序码像这样:

class ShoppingCart {
    private val products = mutableListOf<Product>()

    fun add(product: Product) {
        products.add(product)
    }

    fun totalPrice(): Int {
        return products.sumOf { it.price }
    }
}

这时再回到 ShoppingCartTest 执行测试,假如您看到 Run 面板里出现的是绿色勾勾的话,就表示刚刚增加的程序码都是正确的,一个初步的购物车类别就开发完成了!

小结

透过这种 TDD 的开发流程,我们就可以确保程序码的设计是符合使用者的期待,之後再新增其他程序码时,只要测试没有出现红色错误讯息,就表示我们的程序没有被改坏,是不是更放心了呢?

明天我们就要来看看如何用 TeamCity 来执行 Kotest 的测试案例,以及若测试不通过时会有什麽反应?


<<:  [Day 11] Leetcode 152. Maximum Product Subarray (C++)

>>:  [Day1] JavaScript Drum Kit

[30] 30 天从 Swift 学会 Objective-C:30 天内那些我不懂的部分

这个 30 天我们理解了 C 语言与 Objective-C,然而我其实有不少事情是没有看懂的,铁人...

Use Cisco CCIE 350-701 Dumps For Instant Success

Use Actual Cisco 350-701 Dumps to Learn Faster Pas...

Amazon SageMaker 机器学习线上研讨会

研讨会报名网址:https://supr.link/lmFHX 在这场1小时的线上研讨会中,您可以学...

第一次进入赌场是否要搞懂一下规则 - 永丰金 Shioaji API 初探

事前提要: 本 API 系为 永丰金 PYTHON API,尚未申请的朋友们,有两个方法可以申请 洽...

31.Module

当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 为了解决以上问题,Vuex 允许我们将...