- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。