一般做法(The Usual Setup)
我們有一個(gè)App,展示來(lái)自世界各地的圖片和信息虽风。當(dāng)然棒口,我們的信息應(yīng)該是從API中抓去的。為了做到這點(diǎn)辜膝,通常我們會(huì)有一個(gè)API請(qǐng)求的對(duì)象无牵。
struct FoodService {
func get(completionHandler:Result<[Food]> -> Void){
//make asynchronous API call
//and return appropriate
}
}
既然我們做一個(gè)異步的API調(diào)用,我們不能使用swift內(nèi)置的錯(cuò)誤處理返回一個(gè)正確的response或者throw一個(gè)error厂抖。相反茎毁,使用Result enum
是一個(gè)很好的實(shí)踐。你可以從這里了解到更多關(guān)于Result enum
的信息验游,但是基礎(chǔ)的用法如下:
enum Result<T> {
case Success(T)
case Failure(ErrorType)
}
當(dāng)API調(diào)用成功充岛,攜帶有正確承載對(duì)象的Success result
被傳入到completion handler中。在FoodService中耕蝉,success result包含foot object
的array
崔梗。如果沒(méi)有成功,失敗的result被return垒在,result中會(huì)有一個(gè)失敗發(fā)生時(shí)的error (eg.400)蒜魄。
FoodService的get方法,通常在ViewController中被調(diào)用。ViewController決定如何處理成功或失敗的Result:
//FoodLaLaViewController
var dataSource = [Food]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
getFood()
}
private func getFood() {
//the get function is called here
FoodService().get() { [weak self] result in
switch result {
case .Success(let food):
self?.dataSource = Food
case .Failure(let error):
self?.showError(error)
}
}
}
但是有一個(gè)問(wèn)題....
問(wèn)題(The Problem)
ViewController的getFood () 方法是controller中最重要的方法谈为。畢竟如果API沒(méi)有被正確調(diào)用旅挤,結(jié)果沒(méi)有被正確的處理,controller就不能在屏幕上正確的展示美食伞鲫。為了確保我們的假設(shè)能夠起作用粘茄,為controller寫test就非常重要了。
說(shuō)實(shí)話秕脓,其實(shí)測(cè)試它沒(méi)有那么糟糕....有一個(gè)另辟蹊徑的方法能夠讓你的view controller開(kāi)始測(cè)試柒瓣。
好了,我們準(zhǔn)備開(kāi)始測(cè)試我們的view controller了吠架。
依賴注入(Dependency Injection)
為了測(cè)試ViewController的getFood()方法的正確性芙贫,我們需要注入FoodService到VC中,只需要在function中注入它傍药。
//FoodLaLaViewController
override func viewDidLoad(){
super.viewDidLoad()
//passing in a default food service
getFood(fromService:FoodService())
}
//The FoodService is now injected!
func getFood(fromService service:FoodService) {
service.get() { [weak self] result in
switch result {
case .Success(let food):
self?.dataSource = food
case .Failure(let error):
self?.showError(error)
}
}
}
下面至少給出了測(cè)試的開(kāi)始:
//FoodLaLaViewControllerTests
func testFetchFood() {
viewController.getFood(fromService: FoodService())
// ?? now what?
}
Protocols FTW
下面是我們FoodService的正確版本
struct FoodService {
func get(completionHandler: Result<[Food]> -> Void) {
// make asynchronous API call
// and return appropriate result
}
}
因?yàn)闇y(cè)試的目的磺平,我們需要能夠重寫get function,為了能夠控制被傳入ViewController的Result拐辽。這樣我們能測(cè)試ViewController對(duì)于成功和失敗的處理拣挪。
因?yàn)镕oodService是一個(gè)結(jié)構(gòu)體,我們不能子類化它薛训。作為替代媒吗,你可以猜中仑氛,我們能夠使用protocols乙埃。
實(shí)際上,你能夠把大部分的方法移動(dòng)到一個(gè)簡(jiǎn)單的protocol中:
protocol Gettable {
associatedtype Data
func get(completionHandler: Result<Data> -> Void)
}
注意associated type
锯岖,這個(gè)協(xié)議將會(huì)為所有的service服務(wù)介袜。在這里我們?cè)贔oodService上使用這個(gè)協(xié)議,但是當(dāng)然你能使用相同的協(xié)議在CakeService或DonutService上出吹。通過(guò)使用generic protocol遇伞,你正添加一個(gè)很棒的格式到你應(yīng)用的所有的service上。
現(xiàn)在捶牢,在FoodService中唯一的變化是鸠珠,Service符合一個(gè)Gettable protocol協(xié)議。就是下面:
struct FoodService: Gettable {
// [Food] is inferred here as the correct associated type!
func get(completionHandler: Result<[Food]> -> Void) {
// make asynchronous API call
// and return appropriate result
}
}
使用這中方式另外一個(gè)好處是可讀性。在FoodService里面,你立即可以看到Gettable岸霹,意思很明顯坚芜。你可以用相同的模式來(lái)實(shí)現(xiàn)Creatable、Updatable泌射、Delectable等种呐。你可以立即知道service的作用拂蝎,只要你看到它可缚。
使用協(xié)議(Using the Protocol ??)
所以現(xiàn)在霎迫,是時(shí)候來(lái)重構(gòu)了。在ViewController中帘靡,為了替代 傳FoodService到getFood方法中知给,我們能夠通過(guò)接受一個(gè)Gettable來(lái)約束它。[Food]
作為associated type
// FoodLaLaViewController
override func viewDidLoad() {
super.viewDidLoad()
getFood(fromService: FoodService())
}
func getFood<Service: Gettable where Service.Data == [Food]>(fromService service: Service) {
service.get() { [weak self] result in
switch result {
case .Success(let food):
self?.dataSource = food
case .Failure(let error):
self?.showError(error)
}
}
}
現(xiàn)在我們可以很簡(jiǎn)單的測(cè)試它描姚!
測(cè)試(Test All the Things!)
為了測(cè)試ViewController的getFood方法炼鞠,我們用Gettable進(jìn)行注入,使用[Food]
作為associated type
// FoodLaLaViewControllerTests
class Fake_FoodService: Gettable {
var getWasCalled = false
// you can assign a failure result here
// to test that scenario as well
// the food here is just an array of food for testing purposes
var result = Result.Success(food)
func get(completionHandler: Result<[Food]> -> Void) {
getWasCalled = true
completionHandler(result)
}
}
所以現(xiàn)在轰胁,我們可以通過(guò)諸如Fake_FoodService來(lái)測(cè)試谒主,ViewController調(diào)用一個(gè)service來(lái)返回[Food]
作為成功的result并作為構(gòu)成tableview的datasource。
// FoodLaLaViewControllerTests
class Fake_FoodService: Gettable {
var getWasCalled = false
// you can assign a failure result here
// to test that scenario as well
// the food here is just an array of food for testing purposes
var result = Result.Success(food)
func get(completionHandler: Result<[Food]> -> Void) {
getWasCalled = true
completionHandler(result)
}
}
你現(xiàn)在同樣能為不同的失敗的case寫測(cè)試腳本赃阀。
結(jié)論(Conclusion)
在你的網(wǎng)絡(luò)層使用協(xié)議讓你的code更加通用霎肯、可注入、可測(cè)試榛斯、可讀观游。