[譯] 通過 Quick 和 Nimble 在 Swift 中進行測試驅動開發

左轉︿右轉發表於2018-02-26

在移動開發領域,編寫測試用例並不常見,事實上,大多數移動開發團隊為了加快開發速度,都儘可能地避免編寫測試用例。

作為一個“成熟的”開發者,我嚐到了編寫測試用例的好處,它不僅僅能保證你的 app 的功能符合預期,它也能通過“鎖住”你的程式碼來阻止其他開發者改變你的程式碼。而且測試程式碼和實現程式碼之間的聯絡也有助於新的開發者比較容易地理解和接手專案。

測試驅動開發( TDD )

測試驅動開發( TDD ) 就像一個新的編碼藝術。它遵守下面的遞迴迴圈:

  • 寫一個能導致失敗的測試用例
  • 為通過上述測試寫一些程式碼
  • 重構
  • 重複上述操作,直到我們滿意

讓我為你展示一個簡單的例子,首先思考一下下面函式的實現:

func calculateAreaOfSquare(w: Int, h: Int) -> Double { }
複製程式碼

測試 1: 給兩個數 w=2h=2,預期的面積應該是 4。在這個例子中,這個測試會失敗,因為這個函式目前並沒有實現。

接著我們繼續寫:

func calculateAreaOfSquare(w: Int, h: Int) -> Double { return w * h }
複製程式碼

測試 1 現在通過了!哇哦!

測試 2: 給兩個數 w=-1h=-1,預期的面積應該是 0。在這個例子中,測試會失敗,因為基於目前函式的實現,它會返回 1

讓我們繼續:

func calculateAreaOfSquare(w: Int, h: Int) -> Double { 
    if w > 0 && h > 0 { 
        return w * h 
    } 
    
    return 0
}
複製程式碼

測試 2 現在也通過了!哇哦!

這些操作可以繼續下去,一直到你處理了所有的邊緣情況。接下來你就應該重構你的程式碼,在保證所有的測試用例都能通過的情況下,讓它看起來漂亮簡潔。

基於我們上面討論的,我們意識到,TDD 不僅僅能讓我們寫出高質量的程式碼,它也能讓我們更早的處理邊緣情況。另外,它還能通過不同的分工:一個寫測試用例,一個寫實現程式碼,來進行結對程式設計。你可以在 Dotariel’s Blog Post 找到更多有關於 TDD 的資訊。

你會在本教程中學到什麼?

在教程的結尾,你可以獲得以下的知識:

  • 為什麼 TDD 很棒,有一個基礎的認知。
  • Quick 和 Nimble 如何工作, 有一個基礎的認知。
  • 知道如何使用 Quick 和 Nimble 進行 UI 測試
  • 知道如何使用 Quick 和 Nimble 進行單元測試

前期準備

在我們繼續下去之前,有些前期準備:

  • Swift3 環境和 8.3.3 版本的 Xcode
  • 有 Swift 和 iOS 開發的經驗

配置我們的專案

假設我們要開發一個能夠展示電影列表的 app。 首先開啟 Xcode 並建立一個叫做 MyMovies 的單檢視應用。勾選上 Unit Tests,一旦我們配置好庫和檢視控制器,我們將重新訪問這個目標。

TDD Sample Project

下一步,刪除已存在的 ViewController 並且重新建立一個繼承於UITableViewController 的新類,把它命名為MoviesTableViewController

Main.storyboard 中的 ViewController 刪除,將一個新的UITableViewController 拖進去,讓它繼承於MoviesTableViewController

然後,將 cell 的樣式改為 Subtitle,並且將 identifier 改為 MovieCell,這樣,我們後面就可以同時展示電影的標題和型別了。

[譯] 通過 Quick 和 Nimble 在 Swift 中進行測試驅動開發

不要忘了將這個檢視控制器標記為 initial view controller

[譯] 通過 Quick 和 Nimble 在 Swift 中進行測試驅動開發

這個時候,你的程式碼看上去應該像下面一樣:

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
    }
}
複製程式碼

電影資料

現在,我們需要造出一些電影資料,一會兒,我們需要它們去填充我們的檢視。

Genre Enum

enum Genre: Int {
    case Animation
    case Action
    case None
}
複製程式碼

這個列舉用來標記電影的類別。

Movie Struct

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 & Nimble

Quick 是一個建立在 XCTest 上,為 Swift 和 Objective-C 設計的測試框架. 它通過 DSL 去編寫非常類似於 RSpec 的測試用例。

Nimble 就像是 Quick 的搭檔,它提供了匹配器作為斷言。關於它的更多資訊,請檢視這兒

使用 Carthage 安裝 Quick & Nimble

隨著 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”,並且將兩個框架都新增進去。

現在所有的設定都搞定了!鼓掌撒花!

[譯] 通過 Quick 和 Nimble 在 Swift 中進行測試驅動開發

編寫測試用例 #1

讓我們開始編寫第一個測試用例。已知的是我們有一個列表,一些電影資料。那麼,我們怎麼保證列表檢視顯示正確專案個數?是的!我們需要保證列表檢視的 cell 行數應該和電影資料的個數保持一致。這就是我們第一個需要測試的地方。那麼開始吧!進到 MyMoviesTests 將 XCTest 程式碼全部刪掉,並且將 Quick 和 Nimble 引入進來!

我們必須保證我們的類是 QuickSpec 的子類,當然 QuickSpec 也是 XCTestCase的子類。要清楚的是 QuickNimble 仍然是基於 XCTest 的。 最後,我們還有一件事需要做,那就是需要重寫 spec() 函式, 關於這點,你可以檢視 set of example groups and examples.

import Quick
import Nimble
 
@testable import MyMovies
 
class MyMoviesTests: QuickSpec {
    override func spec() {
    }
}
複製程式碼

這個時候,你需要明白我們將使用一些 itdescribecontext 來編寫我們的測試。 describecontext 只是 it 示例的邏輯分組。

測試 #1 – 預計列表檢視的行數 = 電影資料的個數

首先,引入我們的檢視控制器

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 吧!

完善測試用例 #1

現在,回到 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),一個能確保程式碼良好和高質量的方式。

編寫測試用例 #2

現在,是時候用第二個測試用例來結束這個教程了。 我們意識到,當我們執行 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 相應的資料去展示來完善這個測試用例!

完善測試用例 #2

因為 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 的全部。你也可以在這個工程上去進行更多的嘗試。如果你對教程有任何相關問題,請在下面留下相關評論以便讓我知道。

你可以在這找到相關原始碼


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章