近來在給deepLink功能添加單元測試,發現代碼好些地方耦合嚴重,沒辦法寫單元測試,經過學習發現可使用依賴注入/控制反轉
的方式,把關鍵代碼經過外部注入,從而進行單元測試。typescript
用電腦和CPU的關係來講明一下:電腦的能力由CPU決定,電腦
依賴 CPU
。api
非依賴注入: 可理解爲電腦和CPU是耦合在一塊兒的,建立電腦的時候,就已經決定了使用何種CPU,也就是說電腦的性能已經不可改變。markdown
依賴注入: 可理解爲電腦爲CPU提供了個接口,能夠經過接口更換CPU,從而提高電腦的性能。電腦和CPU再也不耦合在一塊兒了。能夠根據性能需求,更替不一樣的CPU。網絡
非依賴注入ide
class CPU {}
class Computer {
let cpu: CPU = CPU()
}
//VC
let compture = Computer()
複製代碼
依賴注入post
class CPU {}
class Computer {
var cpu: CPU?
init(cpu: CPU) {
self.cpu = cpu
}
}
//VC
let cpu = CPU()
let compture = Computer(cpu: cpu)
複製代碼
依賴注入: 電腦和CPU再也不是強依賴關係。CPU是由外部給予電腦的,電腦和CPU有依賴,可是這個依賴是外部給予,所以咱們能夠說CPU是由外部注入給他的。性能
控制反轉: 而反過來講,電腦搭配何種CPU,具有何種性能,不是他內部自身控制的,而是由外部控制的,外部來決定電腦該具有什麼性能,因此CPU的控制權被由自身控制反轉爲外部控制。單元測試
經過這個簡單的例子,能夠看出其實 依賴注入
和 控制反轉
說的是同一件事情,只是站的角度不一樣而已。學習
哪天調整了CPU類的初始化方法,須要傳個品牌名稱:測試
class CPU {
var name: String
init(name: String) {
self.name = name
}
}
複製代碼
class Computer {
let cpu: CPU = CPU(name: "Intel")
}
let compture = Computer()
複製代碼
class Computer {
var cpu: CPU?
init(cpu: CPU) {
self.cpu = cpu
}
}
let cpu = CPU(name: "Intel")
let compture = Computer(cpu: cpu))
複製代碼
想在電腦上使用不一樣的品牌的CPU:
class CPU1: CPU {}
複製代碼
class Computer {
let cpu: CPU1 = CPU1(name: "AMD")
}
let compture = Computer()
複製代碼
class Computer {
var cpu: CPU?
init(cpu: CPU) {
self.cpu = cpu
}
}
let cpu = CPU1(name: "AMD")
let compture = Computer(cpu: cpu)
複製代碼
核心優勢:利於自動化測試。
給Computer類添加introduction()
方法,並根據不一樣的CPU品牌去測試該方法:
class Computer {
let cpu: CPU = CPU(name: "Intel")
func introduction() -> String {
"I use \(cpu.name) cpu"
}
}
func testIntelCPU() {
let computer = Computer()
XCTAssertEqual(computer.introduction(), "I use Intel cpu")
}
複製代碼
class Computer {
var cpu: CPU?
init(cpu: CPU) {
self.cpu = cpu
}
func introduction() -> String {
"I use \(cpu.name) cpu"
}
}
func testIntelCPU() {
let cpu = CPU(name: "Intel")
let computer = Computer(cpu: cpu)
XCTAssertEqual(computer.introduction(), "I use Intel cpu")
}
func testAMDCPU() {
let cpu = CPU(name: "AMD")
let computer = Computer(cpu: cpu)
XCTAssertEqual(computer.introduction(), "I use AMD cpu")
}
複製代碼
Computer依賴CPU,假如CPU中又有其餘對象,即CPU依賴其餘類,而其餘類又可能有各自的依賴,這樣的話,使用依賴注入就至關有必要了。
打開MainViewController
頁面時,默認顯示LoadingView,此時發起網絡請求,根據請求結果顯示相應的頁面:
final class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view = LoadingView()
//網絡請求
client.fetchSomething(.cacheFirst)
.deliverOnUIQueue()
.onComplete { result in
switch result {
case .success:
view = SuccessView()
case .failure(let error):
view = FailureView()
}
}
}
}
複製代碼
爲了測試3種狀態下的頁面顯示狀況,因此須要將網絡請求部分做爲依賴注入,因此創建一個協議MainPageProvider
,原代碼修改成:
protocol MainPageProvider: AnyObject {
func loadData(completion: @escaping (Result<(), Error>) -> Void)
}
final class MainViewController: UIViewController {
lazy var mainPageProvider: MainPageProvider = self
override func viewDidLoad() {
super.viewDidLoad()
view = LoadingView()
//網絡請求
mainPageProvider.loadData { result in
switch result {
case .success:
view = SuccessView()
case .failure(let error):
view = FailureView()
}
}
}
}
extension MainViewController: MainPageProvider {
func loadData(completion: @escaping (Result<(), Error>) -> Void) {
client.fetchSomething(.cacheFirst)
.deliverOnUIQueue()
.onComplete { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
}
}
複製代碼
在單元測試中,建立一個Mock類MockMainPageProvider
遵循MainPageProvider
協議,從而自定義協議方法
,將網絡請求部分做爲依賴注入到MainViewController
中,這樣就能夠自動化測試3種view的顯示狀況了。
final class MainViewControllerTests: XOTestCase {
var mockMainPageProvider: MockMainPageProvider!
var mainViewController: MainViewController!
override func setUp() {
super.setUp()
mockMainPageProvider = MockMainPageProvider()
mainViewController.mainPageProvider = mockMainPageProvider
}
override func tearDown() {
mockMainPageProvider = nil
mainViewController = nil
super.tearDown()
}
func testMainPageLoadingView() {
mockMainPageProvider.state = .loading
mainViewController.viewDidLoad()
XCTAssertTrue(mainViewController.view is LoadingView)
}
func testMainPageSuccessView() {
mockMainPageProvider.state = .success
mainViewController.viewDidLoad()
XCTAssertTrue(mainViewController.view is SuccessView)
}
func testMainPageSuccessView() {
mockMainPageProvider.state = .failure
mainViewController.viewDidLoad()
XCTAssertTrue(mainViewController.view is FailureView)
}
}
private class MockMainPageProvider: MainPageProvider {
enum State {
case loading, success, failure
}
var state: State = .loading
func loadData(completion: (Result<(), Error>) -> Void) {
switch state {
case .loading:
break
case .success:
completion(.success(()))
case .failure:
completion(.failure(NSError()))
}
}
}
複製代碼