只要是在移動端應用上寫任何類型的測試,這都不是一個受歡迎的選擇,事實上,多數移動端應用開發團隊都儘量省略寫測試的工做,但願藉此教程來節省時間以加速開發進程。ios
自認爲本身是一位技術成熟的開發者,我深入體驗了寫測試帶來的好處,不只確保應用程序內的功能按預期運行,還能夠鎖定本身的代碼,以防止其餘開發人員更改代碼,測試和代碼之間的這種耦合能夠幫助新開發人員輕鬆 onboard 或接管項目。git
Test-Driven Development (TDD) 就像是一個寫 code 的新藝術。它遵循如下循環:github
這邊提供給讀者一個簡單的例子,請參考如下操做範例:bash
func calculateAreaOfSquare(w: Int, h: Int) -> Double { }複製代碼
Test 1:閉包
給定w=2
、h=2
,預期輸出結果會是4
,在上面的代碼當中,這個測試結果會是fail
,由於咱們還沒實做裏面的內容。app
接着,咱們添加一些代碼:框架
func calculateAreaOfSquare(w: Int, h: Int) -> Double { return w * h }複製代碼
第一個測試如今就能夠經過了!ide
(adsbygoogle = window.adsbygoogle || []).push({});複製代碼
Test 2:函數
給定 w=-1
,h=-1
,咱們預期的面積計算結果應該要是0
,在這個範例中,測試又出現fail了,由於按照目前函數的執行方法,它的輸出結果爲1
。單元測試
接着,咱們添加一些代碼:
func calculateAreaOfSquare(w: Int, h: Int) -> Double {
if w > 0 && h > 0 {
return w * h
}
return 0
}複製代碼
如今第二個測試也經過了,太棒了!
持續這個動做,直處處理全部的極端狀況(edge cases),同時,也要進行重構讓代碼變得更好,並經過全部的測試。
根據咱們目前爲止所討論的,咱們瞭解到 TDD 不只能夠創造出更有品質的代碼,並且可讓開發者提早處理極端情況。此外,它還能讓兩個開發人員有效率的進行結對程序設計(pair-programming),一位工程師寫測試,另外一位則編寫可以經過測試的code,你能夠經過 Dotariel的博客文章 瞭解更多細節。
在本教程的尾聲,你應該能帶走下列這些知識:
在進入本文重點以前,如下是一些開發環境準備工做:
假設咱們被指定一個任務是開發一個能夠展現電影資訊的簡單電影應用程序,先啓動Xcode並建立一個新的Single View Application,命名爲MyMovies,並把Unit Tests
勾選起來,當設定完函數庫(libraries)和視圖控制器(view controllers),咱們會從新訪問這個target。
接下來,讓咱們刪除原有的 ViewController
並拖進一個 UITableViewController
,將它命名爲 MoviesTableViewController
,在Main.storyboard
中,刪除 ViewController
,並拉進一個新的 TableViewController
,並將類別設置爲 MoviesTableViewController
。如今,咱們將prototype cell的style設置爲 Subtitle
,將identifier設置爲 MovieCell
,以便咱們稍後能夠顯示電影的 title
和 genre
。
記得要將這個view controller設定爲 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
}複製代碼
這個枚舉(enum)用於判斷咱們的電影類型。
struct Movie {
var title: String
var genre: Genre
}複製代碼
這個電影數據類型(movie data type)用於表示咱們的個別電影數據。
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)
]
}
}複製代碼
這個MoviesDataHelper
類別幫助咱們直接調用 getMovies
方法,以便咱們能夠經過單一調用中獲取電影數據。
咱們須要注意到在這個階段,尚未執行任何TDD,由於目前仍在項目的計劃中執行着,如今讓咱們進入到本教程的主要內容,Quick & Nimble!
Quick是基於 XCTest
構建的測試開發框架,支持 Swift 和 Objective-C,並提供了一個DSL來編寫測試,很是相似於RSpec。
Nimble就像是Quick的夥伴,Nimble提供Matcher作爲Assertion,有關框架的更多訊息,請查看這個連接。
隨着 Carthage
的發展,讓我喜歡 Carthage 更多於 Cocoapods,由於它更分散化,當其中一個 framework 沒法構建時,整個項目仍然能夠編譯。
#CartFile.private
github "Quick/Quick"
github "Quick/Nimble"複製代碼
以上爲 CartFile.private
,用來安裝個人dependencies,若是讀者沒有使用Carthage的任何經驗,請查看此連接。
將CartFile.private
放置在文件夾中,而後運行carthage update
,它將clone這個dependencies,讀者應該會在你的Carthage -> Build -> iOS
文件夾中得到兩個框架。而後,將兩個框架添加到兩個測試target中,接着,還須要去 Build Phases
,點擊左上角的加號,而後選擇 「New Copy Files Phase」,將destination設置爲 「Frameworks」,並在其中添加兩個框架。
開始吧!你如今已經將本文所需的測試函數庫所有設置完成!
讓咱們來開始寫第一個測試,咱們都知道咱們有一個列表,也有一些電影數據,如何確保列視圖顯示的項目數量正確?沒錯!咱們須要確保TableView
的row
與咱們的電影數據的數量相匹配。這就是咱們的第一個測試,因此如今來看看咱們的MyMoviesTests
,刪除XCTest
代碼並導入咱們的Quick和Nimble套件!
這邊必須確保咱們的class是QuickSpec
的子類,它也是本來XCTestCase
的子類,要了解Quick & Nimble
的底層還是XCTest
,在這裏咱們須要作的最後一件事是宣告一個override function spec()
,這裏咱們用來定義一套Example Groups and Examples。
import Quick
import Nimble
@testable import MyMovies
class MyMoviesTests: QuickSpec {
override func spec() {
}
}複製代碼
在這種狀況下,咱們將使用大量的使用it
、describe
和context
來編寫咱們的測試。其中,每一個it表明⼀⼩段測試,describe
和 context
則是 it
示例的邏輯羣集(logical groupings),用來描述你要測試的是什麼。
首先,來引入咱們的 subject
,它是咱們的視圖控制器。
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
}
}
}
}複製代碼
請注意,咱們在這裏放置@testable import MyMovies,這一行基本上就是標示出咱們正在測試的項目目標,而後容許咱們從那裏 import classes
。當咱們測試 TableViewController
的視圖層時,須要從 storyboard
中獲取一個實例。
describe閉包(closure)開始個人第一個測試案例,爲MoviesTableViewController
編寫測試。
beforeEach閉包會在describe閉包中執行,它將在每一個範例開始以前運行,因此你能夠把它看做爲在MoviesTableViewController內的每個測試被執行前,會先運行這段代碼。
_ = subject.view
將視圖控制器放入內存中,它就像是調用viewDidLoad
。
最後,咱們能夠在beforeEach { }
以後添加咱們的 test assertion
,以下所示:
context("when view is loaded") {
it("should have 8 movies loaded") {
expect(subject.tableView.numberOfRows(inSection: 0)).to(equal(8))
}
}複製代碼
這邊來說解一下,咱們有一個context
,它是一個grouped example closure,被標示爲when view is loaded
,接着是主要示例it should have 8 movies loaded
,咱們能夠預測咱們的table view的行數爲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__shou複製代碼
因此你剛剛寫了一個失敗的測試,接下來咱們要來修復它,開始操做TDD吧!
咱們回到主要的MoviesTableViewController
並加載咱們的電影數據!添加這些code以後,再次運行測試,爲本身首次經過測試喝彩吧!
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!
}複製代碼
讓咱們回顧一下,你剛剛寫了一個失敗的測試,而後經過三行代碼修復它,如今它經過了,這就是咱們所說的TDD,能確保高品質、良好 codebas
的方法。
如今是時候用第二個 test case
來替本教程劃下句點,若是咱們運行應用程序,就只是在各個地方設置「title」和「subtitle」,咱們錯過了實際的電影數據!爲此來爲UI寫一個測試吧!
來看看咱們的spec文件。引入一個新的context
調用Table View
。從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"))
}
}複製代碼
如今運行測試會看到它們fail了。
MoviesTableViewController__Table_View__should_show_movie_title_and_genre] : expected to equal <Animation>, got <Subtitle>複製代碼
一樣的,咱們須要修復這個測試!須要給咱們的cell labels顯示正確的數據。
咱們先前將Genre作爲enum之用,這裏來擴充更多的code,因此參考下圖代碼更新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!
}複製代碼
穩!你剛剛經過了你的第二個test case!在這個時刻,咱們來看看能夠重構的內容,嘗試使代碼更簡潔,但仍要能夠經過全部的測試,
咱們刪除空的函數,並將咱們的getMovies()
宣告爲計算屬性(computed property)。
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的執行流程,你能夠繼續使用此項目來嘗試更多的測試工做,若是你對本教程有任何疑問,請評論告知。
對於示例項目,你能夠在 GitHub下載完整的source code。
原文:Test Driven Development (TDD) in Swift with Quick and Nimble