近来在给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()))
}
}
}
复制代码