I. Background:
While adding unit tests to deepLink, I found that some of the code is too tightly coupled to write unit tests. I learned that I can use dependency injection/inversion of control to externally inject key code for unit tests.
3) Dependency Injection/Inversion of Control
Use the relationship between computer and CPU to illustrate: the ability of a computer is determined by the CPU, the computer depends on the CPU.
Non-dependency injection (DI) : It can be understood that the computer and the CPU are coupled together. When the computer is created, the CPU is already determined, that is, the performance of the computer is already immutable.
Dependency injection: the PC provides an interface for the CPU, through which the CPU can be replaced to improve the PC performance. The computer and CPU are no longer coupled together. You can replace different cpus based on performance requirements.
-
Non-dependency injection
class CPU {} class Computer { let cpu: CPU = CPU() } //VC let compture = Computer() Copy the code
-
Dependency injection
class CPU {} class Computer { var cpu: CPU? init(cpu: CPU) { self.cpu = cpu } } //VC let cpu = CPU() let compture = Computer(cpu: cpu) Copy the code
Dependency injection: The computer and CPU are no longer strongly dependent. The CPU is externally given to the computer, the computer has a dependency on the CPU, but this dependency is externally given, so we can say that the CPU is externally injected into it.
Inversion of control: on the other hand, what kind of CPU a computer is equipped with and what kind of performance it has is not controlled by itself, but by the external control. The external decides what performance the computer should have, so the control of the CPU is reversed from its own control to external control.
This simple example shows that dependency injection and inversion of control are talking about the same thing, just from different perspectives.
Non-dependency injection and dependency injection in some cases:
-
If you changed the initialization method of CPU class, you need to pass a brand name:
class CPU { var name: String init(name: String) { self.name = name } } Copy the code
- Non-dependency injection: CPU variables in the Computer need to be modified.
class Computer { let cpu: CPU = CPU(name: "Intel") } let compture = Computer() Copy the code
- Dependency injection: only in VC, when creating Computer objects, inject CPU objects.
class Computer { var cpu: CPU? init(cpu: CPU) { self.cpu = cpu } } let cpu = CPU(name: "Intel") let compture = Computer(cpu: cpu)) Copy the code
-
Want to use a different brand of CPU on your computer:
class CPU1: CPU {} Copy the code
- Non-dependency injection: Again, modify the CPU variables inside the Computer class
class Computer { let cpu: CPU1 = CPU1(name: "AMD") } let compture = Computer() Copy the code
- Dependency injection: There is no need to modify the Computer class, just modify it in VC
class Computer { var cpu: CPU? init(cpu: CPU) { self.cpu = cpu } } let cpu = CPU1(name: "AMD") let compture = Computer(cpu: cpu) Copy the code
-
Core benefits: Easy to automate testing.
Add the Introduction () method to the Computer class and test it for different CPU brands:
- Non-dependency injection: cannot change CPU variables in Computer, can only test the current brand. You can’t automate tests.
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") } Copy the code
- Dependency injection: You can test all brands automatically by passing in cpus of different brands
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") } Copy the code
The Computer depends on the CPU, and if there are other objects in the CPU, that is, the CPU depends on other classes, which may have their own dependencies, then dependency injection is necessary.
4. Examples of dependency injection used in real development:
-
When the MainViewController page is opened, LoadingView is displayed by default. At this time, a network request is initiated and the corresponding page is displayed according to the request result:
- By default, LoadingView is displayed
- Network request successful, SuccessView is displayed
- The network request fails and FailureView is displayed
final class MainViewController: UIViewController {override func viewDidLoad() {super.viewdidLoad () view = LoadingView() // network request client.fetchSomething(.cacheFirst) .deliverOnUIQueue() .onComplete { result in switch result { case .success: view = SuccessView() case .failure(let error): view = FailureView() } } } }Copy the code
-
In order to test the page display in three states, the network request part needs to be used as dependency injection, so a protocol MainPageProvider is established, and the original code is modified as follows:
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() // Network request 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)) } } } }Copy the code
-
In the unit test, create a Mock class MockMainPageProvider that follows the MainPageProvider protocol to customize the protocol method to inject the network request part as a dependency into the MainViewController. This allows you to automatically test the display of the three views.
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())) } } }Copy the code
Five, reference articles:
-
What are dependency injection and inversion of control
-
About Dependency Injection (typescript)