【CodeTest】TDD,BDD及初步使用Quick
学习文章
- 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中国
- XCTestCase /
XCTestExpectation /
measureBlock()
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关于测试的视频,有英文字幕.
WWDC关于测试视频.png接下来,讲一下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安装,可以按照文档利用其它方式.
QuickTemplates.png2. 使用前,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.
}