- 原文地址:Test Driven Development (TDD) in Swift with Quick and Nimble
- 原文做者:LAWRENCE TAN
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:94haox
- 校對者:swants, atuooo
在移動開發領域,編寫測試用例並不常見,事實上,大多數移動開發團隊爲了加快開發速度,都儘量地避免編寫測試用例。前端
做爲一個「成熟的」開發者,我嚐到了編寫測試用例的好處,它不只僅能保證你的 app 的功能符合預期,它也能經過「鎖住」你的代碼來阻止其餘開發者改變你的代碼。並且測試代碼和實現代碼之間的聯繫也有助於新的開發者比較容易地理解和接手項目。android
測試驅動開發( TDD ) 就像一個新的編碼藝術。它遵照下面的遞歸循環:ios
讓我爲你展現一個簡單的例子,首先思考一下下面函數的實現:git
func calculateAreaOfSquare(w: Int, h: Int) -> Double { }
複製代碼
測試 1: 給兩個數 w=2
,h=2
,預期的面積應該是 4
。在這個例子中,這個測試會失敗,由於這個函數目前並無實現。github
接着咱們繼續寫:編程
func calculateAreaOfSquare(w: Int, h: Int) -> Double { return w * h }
複製代碼
測試 1 如今經過了!哇哦!後端
測試 2: 給兩個數 w=-1
,h=-1
,預期的面積應該是 0
。在這個例子中,測試會失敗,由於基於目前函數的實現,它會返回 1
。bash
讓咱們繼續:閉包
func calculateAreaOfSquare(w: Int, h: Int) -> Double {
if w > 0 && h > 0 {
return w * h
}
return 0
}
複製代碼
測試 2 如今也經過了!哇哦!app
這些操做能夠繼續下去,一直到你處理了全部的邊緣狀況。接下來你就應該重構你的代碼,在保證全部的測試用例都能經過的狀況下,讓它看起來漂亮簡潔。
基於咱們上面討論的,咱們意識到,TDD 不只僅能讓咱們寫出高質量的代碼,它也能讓咱們更早的處理邊緣狀況。另外,它還能經過不一樣的分工:一個寫測試用例,一個寫實現代碼,來進行結對編程。你能夠在 Dotariel’s Blog Post 找到更多有關於 TDD 的信息。
在教程的結尾,你能夠得到如下的知識:
在咱們繼續下去以前,有些前期準備:
假設咱們要開發一個可以展現電影列表的 app。 首先打開 Xcode 並建立一個叫作 MyMovies 的單視圖應用。勾選上 Unit Tests
,一旦咱們配置好庫和視圖控制器,咱們將從新訪問這個目標。
下一步,刪除已存在的 ViewController
而且從新建立一個繼承於UITableViewController
的新類,把它命名爲MoviesTableViewController
。
將 Main.storyboard
中的 ViewController
刪除,將一個新的UITableViewController
拖進去,讓它繼承於MoviesTableViewController
。
而後,將 cell 的樣式改成 Subtitle
,而且將 identifier 改成 MovieCell
,這樣,咱們後面就能夠同時展現電影的標題和類型了。
不要忘了將這個視圖控制器標記爲 initial view controller
。
這個時候,你的代碼看上去應該像下面同樣:
import UIKit
class MoviesTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
}
複製代碼
如今,咱們須要造出一些電影數據,一下子,咱們須要它們去填充咱們的視圖。
enum Genre: Int {
case Animation
case Action
case None
}
複製代碼
這個枚舉用來標記電影的類別。
struct Movie {
var title: String
var genre: Genre
}
複製代碼
這個電影數據類型用來描述咱們須要的電影數據。
class MoviesDataHelper {
static func getMovies() -> [Movie] {
return [
Movie(title: "The Emoji Movie", genre: .Animation),
Movie(title: "Logan", genre: .Action),
Movie(title: "Wonder Woman", genre: .Action),
Movie(title: "Zootopia", genre: .Animation),
Movie(title: "The Baby Boss", genre: .Animation),
Movie(title: "Despicable Me 3", genre: .Animation),
Movie(title: "Spiderman: Homecoming", genre: .Action),
Movie(title: "Dunkirk", genre: .Animation)
]
}
}
複製代碼
這個電影數據助手類能夠幫助咱們直接調用 getMovies
方法,因此咱們能夠在單次調用中就能夠得到須要的數據。
提醒一下,到目前爲止,咱們並無在項目中作任何有關 TDD 的配置。如今,讓咱們開始學習這篇教程的主要內容 Quick 和 Nimble 吧!
Quick 是一個創建在 XCTest 上,爲 Swift 和 Objective-C 設計的測試框架. 它經過 DSL 去編寫很是相似於 RSpec 的測試用例。
Nimble 就像是 Quick 的搭檔,它提供了匹配器做爲斷言。關於它的更多信息,請查看這兒
隨着 Carthage 庫的增加,相比 Cocoapods 我愈來愈喜歡 Carthage,由於它更去中心化。即便某一個庫編譯失敗,整個項目依然能夠編譯成功
#CartFile.private
github "Quick/Quick"
github "Quick/Nimble"
複製代碼
上面就是 CartFile.private
中的內容,我經過它來安裝依賴。若是你不熟悉 Carthage,先看看它吧.
將 CartFile.private
拖入你的項目目錄,而後終端運行 carthage update
。這個命令會克隆依賴,成功後,你能夠在 Carthage -> Build -> iOS
找到它們。接着,將兩個框架都添加到測試工程。你須要到 Build Phases 點擊左上方的加號,而且選擇 「New Copy Files Phase」。將它設置爲 「Frameworks」,而且將兩個框架都添加進去。
如今全部的設置都搞定了!鼓掌撒花!
讓咱們開始編寫第一個測試用例。已知的是咱們有一個列表,一些電影數據。那麼,咱們怎麼保證列表視圖顯示正確項目個數?是的!咱們須要保證列表視圖的 cell 行數應該和電影數據的個數保持一致。這就是咱們第一個須要測試的地方。那麼開始吧!進到 MyMoviesTests
將 XCTest 代碼所有刪掉,而且將 Quick 和 Nimble 引入進來!
咱們必須保證咱們的類是 QuickSpec
的子類,固然 QuickSpec
也是 XCTestCase
的子類。要清楚的是 Quick
和 Nimble
仍然是基於 XCTest
的。 最後,咱們還有一件事須要作,那就是須要重寫 spec()
函數, 關於這點,你能夠查看 set of example groups and examples.
import Quick
import Nimble
@testable import MyMovies
class MyMoviesTests: QuickSpec {
override func spec() {
}
}
複製代碼
這個時候,你須要明白咱們將使用一些 it
, describe
和 context
來編寫咱們的測試。 describe
和 context
只是 it
示例的邏輯分組。
首先,引入咱們的視圖控制器
import Quick
import Nimble
@testable import MyMovies
class MyMoviesTests: QuickSpec {
override func spec() {
var subject: MoviesTableViewController!
describe("MoviesTableViewControllerSpec") {
beforeEach {
subject = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MoviesTableViewController") as! MoviesTableViewController
_ = subject.view
}
}
}
}
複製代碼
須要注意的是,咱們有一個對 MyMovies
的 @testable
引用,這行代碼的目的是標記着咱們在測試哪一個項目,而且容許咱們引用那裏的類。因爲咱們須要測試控制器的視圖層,因此須要從 storyboard 抓取一個實例。
describe 閉包應該是咱們爲 MoviesTableViewController
而寫的第一個組合測試用例。
beforeEach 閉包將在 describe 閉包中全部例子執行以前運行。因此你能夠在其中寫一些須要在 MoviesTableViewController
執行時首先運行的測試。
_ = subject.view
會將視圖控制器放入內存,它相似於調用 viewDidLoad
。
最後,咱們能夠在 beforeEach { }
以後添加測試斷言。好比:
context("when view is loaded") {
it("should have 8 movies loaded") {
expect(subject.tableView.numberOfRows(inSection: 0)).to(equal(8))
}
}
複製代碼
讓咱們一步步來看。首先,咱們有一個被標記爲 when view is loaded
組合示例閉包 context
;接着,咱們還有一個主要的示例 it should have 8 movies loaded
;而後,咱們預計或者斷言列表視圖的 cell 有 8 行。經過按 CMD+U 或者 Product -> Test 運行測試用例,而後你會在控制面板上看到下面信息:
MoviesTableViewController__when_view_is_loaded__should_have_8_movies_loaded] : expected to equal <8>, got <0>
Test Case '-[MyMoviesTests.MoviesTableViewControllerSpec MoviesTableViewController__when_view_is_loaded__should_have_8_movies_loaded]' failed (0.009 seconds).
複製代碼
因此,你只是寫了一個並不完善的測試用例。開始 TDD 吧!
如今,回到 MoviesTableViewController
,加載電影數據! 而後再從新運行測試用例,接着,以前寫的測試用例經過了!
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MoviesDataHelper.getMovies().count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell")
return cell!
}
複製代碼
總結一下,首先你寫了一個不完善的測試,而後經過 3 行代碼完善了它,而且測試經過了,這就是爲何咱們將它稱爲測試驅動開發(TDD),一個能確保代碼良好和高質量的方式。
如今,是時候用第二個測試用例來結束這個教程了。 咱們意識到,當咱們運行 app 的時候,咱們只是在每一個地方設置 「title」 和 「subtitle」。可是咱們並無驗證它顯示的是否是咱們實際的數據!因此,爲 UI 也寫個測試用例吧。
進入 spec 文件。 添加一個新的 context
並把它稱爲 Table View
。從 列表視圖抓取第一個 cell ,而且測試它展現的數據是否和實際應該展現的數據相同。
context("Table View") {
var cell: UITableViewCell!
beforeEach {
cell = subject.tableView(subject.tableView, cellForRowAt: IndexPath(row: 0, section: 0))
}
it("should show movie title and genre") {
expect(cell.textLabel?.text).to(equal("The Emoji Movie"))
expect(cell.detailTextLabel?.text).to(equal("Animation"))
}
}
複製代碼
測試運行後,會獲得下面的失敗信息。
MoviesTableViewController__Table_View__should_show_movie_title_and_genre] : expected to equal <Animation>, got <Subtitle>
複製代碼
來吧,讓咱們經過給 cell 相應的數據去展現來完善這個測試用例!
由於 Genre 是枚舉,咱們須要爲它添加不一樣的描述。因此咱們須要更新 Movie
類:
struct Movie {
var title: String
var genre: Genre
func genreString() -> String {
switch genre {
case .Action:
return "Action"
case .Animation:
return "Animation"
default:
return "None"
}
}
}
複製代碼
一樣 cellForRow
方法也須要更新:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell")
let movie = MoviesDataHelper.getMovies()[indexPath.row]
cell?.textLabel?.text = movie.title
cell?.detailTextLabel?.text = movie.genreString()
return cell!
}
複製代碼
哇哦!第二個測試用例經過啦!此時,讓咱們看看能不能經過重構讓代碼更加清晰,固然,仍然是在保持測試用例能夠經過的基礎上。移除空函數,而且將 getMovies()
聲明爲計算屬性。
class MoviesTableViewController: UITableViewController {
var movies: [Movie] {
return MoviesDataHelper.getMovies()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell")
let movie = movies[indexPath.row]
cell?.textLabel?.text = movie.title
cell?.detailTextLabel?.text = movie.genreString()
return cell!
}
}
複製代碼
試試吧,從新運行測試,它依然是能夠經過的。
咱們作了什麼?
這大概就是 TDD 的所有。你也能夠在這個工程上去進行更多的嘗試。若是你對教程有任何相關問題,請在下面留下相關評論以便讓我知道。
你能夠在這找到相關源碼
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。