In order to have qualified code, writing Unit Test is an important skill that every developer needs to know. Here are some tips for writing it easier.
1. Using Dependency Injection and Protocols:
Let’s see how can we write Unit Test for class User here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class DataProvider { func getListUsers (completion: @escaping (Data?, Error?) -> Void ) { let request = URLRequest (url: URL (string: "url-to-get-list-users" )!) let task = URLSession .shared.dataTask(with: request) { data, _ , error in completion(data, error) } task.resume() } } class User { func listActiveIDs (completion: @escaping ([String]?) -> Void ) { DataProvider ().getListUsers { data, error in guard let data = data else { completion(nil ) return } completion(data.activeUserIDs()) } } }
There are 2 cases need to be tested:
listActiveIDs_havingData
listActiveIDs_noData
But how can we control the output of DataProvider().getListUsers?
That is time for you to use Dependency Injection and Protocols to control the output. We can refactor the code above a little bit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 protocol DataProviderProtocol { func getListUsers (completion: @escaping (Data?, Error?) -> Void ) } class DataProvider : DataProviderProtocol { func getListUsers (completion: @escaping (Data?, Error?) -> Void ) { let request = URLRequest (url: URL (string: "url-to-get-list-users" )!) let task = URLSession .shared.dataTask(with: request) { data, _ , error in completion(data, error) } task.resume() } } class User { let _dataProdiver: DataProviderProtocol init (dataProdiver: DataProviderProtocol ) { _dataProdiver = dataProdiver } func listActiveIDs (completion: @escaping ([String]?) -> Void ) { _dataProdiver.getListUsers { data, error in guard let data = data else { completion(nil ) return } completion(data.activeUserIDs()) } } }
See? Now in Test Target, we can use another mock DataProvider class to control the outputs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class MockDataProvider : DataProvider { let _data: Data? init (data: Data? ) { _data = data } override func getListUsers (completion: @escaping (Data?, Error?) -> Void ) { completion(_data, nil ) } } class UserTest : XCTestCase { func testListActiveIDs_havingData () { let mockDataProvider = MockDataProvider (data: Data .mockUsers()) let user = User (dataProdiver: mockDataProvider) user.listActiveIDs { ids in XCTAssertNotNil (ids) } } func testListActiveIDs_noData () { let mockDataProvider = MockDataProvider (data: nil ) let user = User (dataProdiver: mockDataProvider) user.listActiveIDs { ids in XCTAssertNil (ids) } } }
It’s very easy and clean. You should be aware of this pattern from the beginning, then you don’t need to refactor your code just to write Unit Test.
2. Mock system/framework functions:
In a real application, we always need to use functions from system or framework. Because they are all not able to be modified, we need another solution to mock it.
See this example, we need to test whether the device is an iPad.
1 2 3 4 5 6 7 class DeviceHelper { func isIPad () -> Bool { return UIDevice .current.userInterfaceIdiom == .pad } } let isIPad = DeviceHelper ().isIPad()
Because that value will be returned from Apple framework, so you cannot directly modify it.
How to control output of UIDevice.current.userInterfaceIdiom?
We will use a protocol that has the same function as the framework.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 protocol DeviceSupportProtocol { var userInterfaceIdiom: UIUserInterfaceIdiom { get } } extension UIDevice : DeviceSupportProtocol {}class DeviceHelper { let _device: DeviceSupportProtocol init (device: DeviceSupportProtocol ) { _device = device } func isIPad () -> Bool { return _device.userInterfaceIdiom == .pad } } let isIPad = DeviceHelper (device: UIDevice .current).isIPad()class MockDevice : DeviceSupportProtocol { var userInterfaceIdiom: UIUserInterfaceIdiom { return .phone } }
Because in the framework, UIDevice already defined the function in the protocol, so we don’t need to write anything when conforming to the protocol.
You can get the exact function by entering Apple framework. (Copy function name except open access control).
Now you just need to use Dependency Injection. Done ╰ (▔∀▔) ╯