學習文章
- TDD的iOS開發初步以及Kiwi使用入門
- Kiwi 使用進階 Mock, Stub, 參數捕獲和異步測試
- 蘋果官方介紹
[蘋果官方文檔](https://developer.apple.com/library/prerelease/tvos/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/01-introduction.html#//apple_ref/doc/uid/TP40014132)
- objc中國
- XCTest?Case /
XCTest?Expectation /
measure?Block()
TDD的必要性
以下引自王巍大神的博客:
測試驅動開發(Test Driven Development,以下簡稱TDD)是保證代碼質量的不二法則,也是先進程序開發的共識。
測試驅動開發并不是一個很新鮮的概念了。軟件開發工程師們(當然包括你我)最開始學習程序編寫時,最喜歡干的事情就是編寫一段代碼,然后運行觀察結果是否正確。如果不對就返回代碼檢查錯誤,或者是加入斷點或者輸出跟蹤程序并找出錯誤,然后再次運行查看輸出是否與預想一致。如果輸出只是控制臺的一個簡單的數字或者字符那還好,但是如果輸出必須在點擊一系列按鈕之后才能在屏幕上顯示出來的東西呢?難道我們就只能一次一次地等待編譯部署,啟動程序然后操作UI,一直點到我們需要觀察的地方么?這種行為無疑是對美好生命和絢麗青春的巨大浪費。于是有一些已經浪費了無數時間的資深工程師們突然發現,原來我們可以在代碼中構建出一個類似的場景,然后在代碼中調用我們之前想檢查的代碼,并將運行的結果與我們的設想結果在程序中進行比較,如果一致,則說明了我們的代碼沒有問題,是按照預期工作的。
TDD是一種相對于普通思維的方式來說,比較極端的一種做法。我們一般能想到的是先編寫業務代碼,然后為其編寫測試代碼,用來驗證產品方法是不是按照設計工作。而TDD的思想正好與之相反,在TDD的世界中,我們應該首先根據需求或者接口情況編寫測試,然后再根據測試來編寫業務代碼,而這其實是違反傳統軟件開發中的先驗認知的。但是我們可以舉一個生活中類似的例子來說明TDD的必要性:有經驗的砌磚師傅總是會先拉一條垂線,然后沿著線砌磚,因為有直線的保證,因此可以做到筆直整齊;而新入行的師傅往往二話不說直接開工,然后在一階段完成后再用直尺垂線之類的工具進行測量和修補。TDD的好處不言自明,因為總是先測試,再編碼,所以至少你的所有代碼的public部分都應該含有必要的測試。另外,因為測試代碼實際是要使用產品代碼的,因此在編寫產品代碼前你將有一次深入思考和實踐如何使用這些代碼的機會,這對提高設計和可擴展性有很好的幫助,試想一下你測試都很難寫的接口,別人(或者自己)用起來得多糾結。在測試的準繩下,你可以有目的有方向地編碼;另外,因為有測試的保護,你可以放心對原有代碼進行重構,而不必擔心破壞邏輯。這些其實都指向了一個最終的目的:讓我們快樂安心高效地工作。
BDD的測試思想
以下同樣引自王巍大神的博客:
XCTest(作者注:蘋果官方測試框架)是基于OCUnit的傳統測試框架,在書寫性和可讀性上都不太好。在測試用例太多的時候,由于各個測試方法是割裂的,想在某個很長的測試文件中找到特定的某個測試并搞明白這個測試是在做什么并不是很容易的事情。所有的測試都是由斷言完成的,而很多時候斷言的意義并不是特別的明確,對于項目交付或者新的開發人員加入時,往往要花上很大成本來進行理解或者轉換。另外,每一個測試的描述都被寫在斷言之后,夾雜在代碼之中,難以尋找。使用XCTest測試另外一個問題是難以進行mock或者stub,而這在測試中是非常重要的一部分。
行為驅動開發(BDD)正是為了解決上述問題而生的,作為第二代敏捷方法,BDD提倡的是通過將測試語句轉換為類似自然語言的描述,開發人員可以使用更符合大眾語言的習慣來書寫測試,這樣不論在項目交接/交付,或者之后自己修改時,都可以順利很多。如果說作為開發者的我們日常工作是寫代碼,那么BDD其實就是在講故事。一個典型的BDD的測試用例包活完整的三段式上下文,測試大多可以翻譯為
Given..When..Then
的格式,讀起來輕松愜意。BDD在其他語言中也已經有一些框架,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber。而在objc社區中BDD框架也正在欣欣向榮地發展,得益于objc的語法本來就非常接近自然語言,再加上C語言宏的威力,我們是有可能寫出漂亮優美的測試的。在objc中,現在比較流行的BDD框架有cedar,specta和Kiwi。其中個人比較喜歡Kiwi,使用Kiwi寫出的測試看起來大概會是這個樣子的:
describe(@"Team", ^{
context(@"when newly created", ^{
it(@"should have a name", ^{
id team = [Team team];
[[team.name should] equal:@"Black Hawks"];
});
it(@"should have 11 players", ^{
id team = [Team team];
[[[team should] have:11] players];
});
});
});
我們很容易根據上下文將其提取為Given..When..Then的三段式自然語言
Given a team, when newly created, it should have a name, and should have 11 players
Quick + Nimble In Swift
就像王巍大神在博客中所提到的,iOS和Mac開發中,也誕生了不少很棒的第三方BDD測試框架,如OC時代的:
Swift時代應運而生的:
他們之間的比較和簡單介紹,可以參見行為驅動開發
另外,推薦大家觀看一下歷屆WWDC關于測試的視頻,有英文字幕.
接下來,講一下Quick + Nimble在Swift中的使用,學習自Quick文檔.
1. CocoaPods安裝Quick + Nimble
如果不喜歡用CocoaPods安裝,可以按照文檔利用其它方式.
pods描述文件(記得去官網實時更新版本號
Quick):
# Podfile
use_frameworks!
def testing_pods
pod 'Quick', '~> 0.8.0'
pod 'Nimble', '3.0.0'
end
target 'MyTests' do
testing_pods
end
target 'MyUITests' do
testing_pods
end
[可選].利用Alcatraz安裝Quick測試文件模板
如果不喜歡用Alcatraz安裝,可以按照文檔利用其它方式.
2. 使用前,Xcode的相關設置
- 工程中的
defines module
設置為YES
- 用public來修飾需要測試的struck,class等,還有其中的變量和方法
- 在你的測試Target中導入app target 的module
3. 有效測試的三板斧思路:Arrange, Act, and Assert
我們利用蘋果官方XCTest框架來演示這節.
其一,了解一下XCTest,
其二,可以借此體會Quick+Nimble的優勢.
相關代碼:
Banana.swift
public class Banana {
private var isPeeled = false
public init() {
}
public func peel() {
isPeeled = true
}
public var isEdible : Bool {
return isPeeled
}
}
BananaTests.swift
import XCTest
import UseQuick
class BananaTest: XCTestCase {
// 為了準確定位測試內容,方法名應該能反映出測試內容
func testPeel_makesTheBananaEdible() {
// Arrange:
let banana = Banana()
// Act:
banana.peel()
// Assert:
XCTAssertTrue(banana.isEdible)
}
}
Offer.swift
public func offer(banana : Banana) -> String {
if banana.isEdible {
return "Hey, want a banana ?"
} else {
return "Hey, want me to peel a banana for u ?"
}
}
OfferTests.swift
import XCTest
import UseQuick
class OfferTests: XCTestCase {
var banana : Banana!
override func setUp() {
super.setUp()
banana = Banana()
}
override func tearDown() {
banana = nil
super.tearDown()
}
func testOffer_whenTheBananaIsPeeled_offersTheBanana() {
// Arrange:
banana.peel()
// Act:
let message = offer(banana)
// Assert:
XCTAssertEqual(message, "Hey, want a banana ?")
}
func testOffer_whenTheBananaIsntPeeled_offersToPeelTheBanana() {
// Act:
let message = offer(banana)
// Assert:
XCTAssertEqual(message, "Hey, want me to peel a banana for u ?")
}
}
以上需要注意:
- 測試類的后綴一般有命名規范,如蘋果官方的測試類文件都以
Tests
結尾,而Quick以Spec
結尾.測試的方法,蘋果官方以test
作為前綴,這樣,編譯器就能意識到它是一個測試方法. - 一開始學習測試,三板斧思路:Arrange, Act, and Assert對我們是很有幫助的
- 測試方法名應該能反映出測試內容
- 蘋果官方的測試文件模板給我們提供了
setUp
和tearDown
方法,就像注釋中所說,前者是在所有測試方法執行前調用,后者是所有測試方法執行完畢后調用,我們可以用以管理一些對象的生命周期.
4.Nimble Assertions
為什么要使用Nimble?
Nimble有更簡潔,更接近自然語言的語法,更詳細的測試信息提示,詳見Clearer Tests Using Nimble Assertions
相關代碼
Monkey.swift
public enum MonkeyIntelligent {
case ExtremelySilly
case NotSilly
case VerySilly
}
public class Monkey: Equatable {
var name : String?
var silliness : MonkeyIntelligent?
public init(name: String, silliness: MonkeyIntelligent) {
self.name = name
self.silliness = silliness
}
}
public func ==(lhs: Monkey, rhs: Monkey) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
SilliestMonkey.swift
public func silliest(monkeys: [Monkey]) -> [Monkey] {
return monkeys.filter { $0.silliness == .VerySilly || $0.silliness == .ExtremelySilly }
}
public func monkeyContains<T : Equatable>(array : [T], object : T?) -> Bool {
for temp in array {
if temp == object {
return true
}
}
return false
}
SilliestMonkeyTests.swift
import XCTest
import UseQuick
import Nimble
class SilliestMonkeyTests: XCTestCase {
func testSilliest_whenMonkeysContainSillyMonkeys_theyreIncludedInTheResult() {
// Arrange:
let kiki = Monkey(name: "Kiki", silliness: .ExtremelySilly)
let carl = Monkey(name: "Carl", silliness: .NotSilly)
let jane = Monkey(name: "Jane", silliness: .VerySilly)
// Act:
let sillyMonkeys = silliest([kiki, carl, jane])
// Assert:
// XCTAssertTrue(monkeyContains(sillyMonkeys, object: kiki))
// XCTAssertTrue(monkeyContains(sillyMonkeys,object: kiki), "Expected sillyMonkeys to contain 'Kiki'")
// 使用Nimble
expect(sillyMonkeys).to(contain(kiki))
}
}
5.Quick
同理,為什么要使用Quick?
還記得在測試中,給方法起那長長的名字么...,比如,前文中的testSilliest_whenMonkeysContainSillyMonkeys_theyreIncludedInTheResult
,用Quick,或者其他BDD的框架,就不用在這樣做了.
事實上,Quick讓我們能夠寫出更具有描述性的測試,并且,簡化我們的代碼,尤其是arrange
階段的代碼.
it用于描述測試的方法名
import Quick
import Nimble
import Sea
class DolphinSpec: QuickSpec {
override func spec() {
it("is friendly") {
expect(Dolphin().isFriendly).to(beTruthy())
}
it("is smart") {
expect(Dolphin().isSmart).to(beTruthy())
}
}
}
describe用于描述類和方法
import Quick
import Nimble
class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
describe("its click") {
it("is loud") {
let click = Dolphin().click()
expect(click.isLoud).to(beTruthy())
}
it("has a high frequency") {
let click = Dolphin().click()
expect(click.hasHighFrequency).to(beTruthy())
}
}
}
}
}
beforeEach/afterEach相當于setUp/tearDown,beforeSuite/afterSuite相當于全局setUp/tearDown
import Quick
import Nimble
class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
var dolphin: Dolphin!
beforeEach {
dolphin = Dolphin()
}
describe("its click") {
var click: Click!
beforeEach {
click = dolphin.click()
}
it("is loud") {
expect(click.isLoud).to(beTruthy())
}
it("has a high frequency") {
expect(click.hasHighFrequency).to(beTruthy())
}
}
}
}
}
context用于指定條件或狀態
class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
var dolphin: Dolphin!
beforeEach {
dolphin = Dolphin()
}
describe("its click") {
var click: Click!
beforeEach {
click = dolphin.click()
}
it("is loud") {
expect(click.isLoud).to(beTruthy())
}
it("has a high frequency") {
expect(click.hasHighFrequency).to(beTruthy())
}
}
}
}
}
我們來對比以下蘋果官方用法和Quick用法
蘋果:
func testDolphin_click_whenTheDolphinIsNearSomethingInteresting_isEmittedThreeTimes() {
// ...
}
Quick:
describe("a dolphin") {
describe("its click") {
context("when the dolphin is near something interesting") {
it("is emitted three times") {
// ...
}
}
}
}
由此,Quick的可讀性,書寫性的優勢,可見一斑.
屏蔽測試
在方法名前加'x',可以屏蔽此方法的測試,如:
xdescribe("its click") {
// ...none of the code in this closure will be run.
}
xcontext("when the dolphin is not near anything interesting") {
// ...none of the code in this closure will be run.
}
xit("is only emitted once") {
// ...none of the code in this closure will be run.
}
集中測試
在方法名前加'f',可以只測試這些加'f'的測試,如:
fit("is loud") {
// ...only this focused example will be run.
}
it("has a high frequency") {
// ...this example is not focused, and will not be run.
}
fcontext("when the dolphin is near something interesting") {
// ...examples in this group are also focused, so they'll be run.
}