During the development of an ios application, the developer may face the task of unit-testing code. This is exactly the task I faced.
Let's say we have an application with authentication. For authentication, it is responsible authentication service - AuthenticationService. For example, it will have two methods, both authenticate the user, but one is synchronous and the other is asynchronous:
protocol AuthenticationService { typealias Login = String typealias Password = String typealias isSucces = Bool /// /// /// - Parameters: /// - login: /// - password: /// - Returns: func authenticate(with login: Login, and password: Password) -> isSucces /// /// /// - Parameters: /// - login: /// - password: /// - authenticationHandler: Callback(completionHandler) func asyncAuthenticate(with login: Login, and password: Password, authenticationHandler: @escaping (isSucces) -> Void) }
There is a viewController that will use this service:
class ViewController: UIViewController { var authenticationService: AuthenticationService! var login = "Login" var password = "Password" /// , var aunthenticationHandler: ((Bool) -> Void) = { (isAuthenticated) in print("\n :") isAuthenticated ? print(" ") : print(" ") } override func viewDidLoad() { super.viewDidLoad() authenticationService = AuthenticationServiceImplementation() // - , , .. viewController performAuthentication() performAsyncAuthentication() } func performAuthentication() { let isAuthenticated = authenticationService.authenticate(with: login, and: password) print(" :") isAuthenticated ? print(" ") : print(" ") } func performAsyncAuthentication() { authenticationService.asyncAuthenticate(with: login, and: password, and: aunthenticationHandler) } }
We need to test the viewController.
Since we do not want our tests to depend on any other objects besides the class of our viewController, we will be mocking all its dependencies. To do this, we make a stub authentication service. It would look like this:
class MockAuthenticationService: AuthenticationService { var emulatedResult: Bool? // , var receivedLogin: AuthenticationService.Login? // var receivedPassword: AuthenticationService.Password? // var receivedAuthenticationHandler: ((AuthenticationService.isSucces) -> Void)? // , func authenticate(with login: AuthenticationService.Login, and password: AuthenticationService.Password) -> AuthenticationService.isSucces { receivedLogin = login receivedPassword = password return emulatedResult ?? false } func asyncAuthenticate(with login: AuthenticationService.Login, and password: AuthenticationService.Password, and authenticationHandler: @escaping (AuthenticationService.isSucces) -> Void) { receivedLogin = login receivedPassword = password receivedAuthenticationHandler = authenticationHandler } }
By manually writing as much code for each dependency, a very unpleasant task (it is especially pleasant to rewrite them when the protocol changes in dependencies). I started looking for a solution to this problem. I thought to find an analogue of the mockito (I looked at my colleagues involved in android development). During the search, I learned that swift supports read-only reflection (in runtime, we can only find out information about objects, change the behavior of an object, we cannot). Therefore, there is no such library. Desperate, I asked a question on a toaster. The solution suggested: Vyacheslav Beltyukov and the Man with the Bear (ManWithBear) .
We will generate mocks using Sourcery. Sourcery uses templates to generate code. There are several standard ones that AutoMockable is suitable for our purposes.
Let's get down to business:
1) Add to our project pod 'Sourcery'.
2) Customize RunScript for our project.
$PODS_ROOT/Sourcery/bin/sourcery --sources . --templates ./Pods/Sourcery/Templates/AutoMockable.stencil --output ./SwiftMocking
Where:
"$ PODS_ROOT / Sourcery / bin / sourcery" is the path to the Sourcery executable file.
"--sources." - Specifying what to analyze for code generation (a dot indicates the current project folder, that is, we will see if we need to generate mocks for each file of our project).
"--templates ./Pods/Sourcery/Templates/AutoMockable.stencil" - the path to the code generation pattern.
"--output ./SwiftMocking" is a place where the result of code generation will be stored (our project is called SwiftMocking).
3) Add the file AutoMockable.swift to our project:
/// , protocol AutoMockable {}
4) The protocols that we want to mock must inherit from AutoMockable. In our case, we are inherited by the AuthenticationService:
protocol AuthenticationService: AutoMockable {
5) Build a project. In the folder the path to which we specified as the --ouput parameter, the AutoMockable.generated.swift file is generated, in which the generated mocks will be located. All subsequent mocks will be added to this file.
6) Add this file to our project. Now we can use our stubs.
Let's see what is generated for the authentication service protocol.
class AuthenticationServiceMock: AuthenticationService { //MARK: - authenticate var authenticateCalled = false var authenticateReceivedArguments: (login: Login, password: Password)? var authenticateReturnValue: isSucces! func authenticate(with login: Login, and password: Password) -> isSucces { authenticateCalled = true authenticateReceivedArguments = (login: login, password: password) return authenticateReturnValue } //MARK: - asyncAuthenticate var asyncAuthenticateCalled = false var asyncAuthenticateReceivedArguments: (login: Login, password: Password, authenticationHandler: (isSucces) -> Void)? func asyncAuthenticate(with login: Login, and password: Password, and authenticationHandler: @escaping (isSucces) -> Void) { asyncAuthenticateCalled = true asyncAuthenticateReceivedArguments = (login: login, password: password, authenticationHandler: authenticationHandler) } }
Perfectly. Now we can use stubs in our tests:
import XCTest @testable import SwiftMocking class SwiftMockingTests: XCTestCase { var viewController: ViewController! var authenticationService: AuthenticationServiceMock! override func setUp() { super.setUp() authenticationService = AuthenticationServiceMock() viewController = ViewController() viewController.authenticationService = authenticationService viewController.login = "Test login" viewController.password = "Test password" } func testPerformAuthentication() { // given authenticationService.authenticateReturnValue = true // when viewController.performAuthentication() // then XCTAssert(authenticationService.authenticateReceivedArguments?.login == viewController.login, " ") XCTAssert(authenticationService.authenticateReceivedArguments?.password == viewController.password, " ") XCTAssert(authenticationService.authenticateCalled, " ") } func testPerformAsyncAuthentication() { // given var isAuthenticated = false viewController.aunthenticationHandler = { isAuthenticated = $0 } // when viewController.performAsyncAuthentication() authenticationService.asyncAuthenticateReceivedArguments?.authenticationHandler(true) // then XCTAssert(authenticationService.asyncAuthenticateCalled, " ") XCTAssert(authenticationService.asyncAuthenticateReceivedArguments?.login == viewController.login, " ") XCTAssert(authenticationService.asyncAuthenticateReceivedArguments?.password == viewController.password, " ") XCTAssert(isAuthenticated, " ") } }
Sourcery writes stubs for us, saving our time. This utility has other applications: generating Equatable extensions for structures in our projects (so that we can compare the objects of these structures).
Source: https://habr.com/ru/post/332120/