只要是在移动端应用上写任何类型的测试,这都不是一个受欢迎的选择,事实上,多数移动端应用开发团队都尽量省略写测试的工做,但愿借此教程来节省时间以加速开发进程。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