Photo by on Unsplash. Kristopher Roller During their Developer careers, coders may encounter a Wirex fintech app that combines traditional money and cryptocurrencies onto one platform. For us in the know, it is crucial to understand what’s going on with the app from the user’s perspective; Especially when 4 million active clients trust it with their money. While a checks one particular place (scene/function/module), check the whole flow that the user goes through. unit test end-to-end tests are a convenient tool that can be used for this purpose. They launch an app and start interacting with all the visible elements going from one screen to another the User Interface (UI) Tests same way as the user does. Pros and Cons of Adding UI Tests Advantages of UI testing: It helps to functionality and find critical bugs🪲. check UI Combined with Unit tests, UI tests can give us maximum . code coverage It checks the app from a . user’s perspective UI tests can be shown to customers to explain the necessity of testing. Concerns Regarding UI testing: : Building a UI Test scenario requires preparation of the UI Elements and takes considerable time to create all the dependencies between our screen flows. Harder to build : Each test takes about 13–34 seconds to run in total, all 21 UI tests take , whereas all our 42 Unit tests only take Longer running time 8 minutes 2 seconds. : Detecting the issue is a good thing, but when you want to catch a bug that’s far away from the code, it’s much harder to fix. When a unit test fails, it shows you the exact way it was broken. And yes, all changes in the UI require us to modify the UI test as well. Harder to fix and maintain Page Object Pattern Once we started to write UI tests, one thing became obvious — a will the code and make it difficult to maintain. vast usage of string constants clutter All components visible on the screen are represented as objects and the most common way to identify them is to use string as an identifier. XCUIElement pattern is an effective solution for this problem. This is the description of our implementation. Page Object Every screen is represented by one and every conforms to Page protocol: PageObject PageObject import XCTest protocol Page { var app: XCUIApplication { get } var view: XCUIElement { get } init(app: XCUIApplication) } This is how the looks like👍🏻: PageObject import XCTest class LoginPage: Page { var app: XCUIApplication var view: XCUIElement private let loginButton: XCUIElement private let emailTextField: XCUIElement private let passwordTextField: XCUIElement required init(app: XCUIApplication) { self.app = app view = app.otherElements["Login_Scene"] emailTextField = app.textFields.firstMatch passwordTextField = app.secureTextFields.firstMatch loginButton = app.buttons["Log In"].firstMatch } @discardableResult func tapEmailTextField() -> Self { emailTextField.tap() return self } @discardableResult func typeInEmailTextField(_ text: String) -> Self { emailTextField.typeText(text) return self } @discardableResult func tapLoginButton() -> DashboardPage { loginButton.tap() return DashboardPage(app: app) } } Pay attention to the line. Unlike buttons, images, or text fields, the main view should have an . view = app.otherElements[“Login_Scene”] explicitly set identifier We set it in the of every scene, in the method: UIViewController viewDidLoad override func viewDidLoad() { super.viewDidLoad() view.accessibilityIdentifier = "Login_Scene" } Another thing to mention about is the return type of every function — it is . PageObject either Self or another PageObject As a result, we will be able to chain our methods. Here is what the resulting test may look like: func testLogIn() { app.launch() // let's assume that login page is the first one that is shown on start let loginPage = LoginPage(app: app) let dashboardPage = loginPage .checkLoginButtonIsDisabled() .tapEmailTextField() .typeInEmailTextField("newUser@gmail.com") .tapPasswordTextField() .typeInPasswordTextField("password") .checkLoginButtonIsEnabled() .tapLoginButton() guard dashboardPage.view.waitForExistence(timeout: 20) else { return XCTFail("Dashboard Scene must have been shown") } } So it’s really quite simple, isn’t it? We can chain our screens to create concise, readable UI Tests that are also easier to maintain. We can reuse these for different flows, we want to check in our UI Test, and if we make some changes in our UI, we only need to fix it in one place. Page Objects If you want to start writing in your project, start by creating . It will save you a lot of time and mental resources 😌 in the future. UITests Page Objects Communication With API Should you use a real server or a mocked one? The second problem we’ve faced was the UI test’s communication with the backend API. We had to choose either to use our development server or to create mocks that imitate API requests. We decided to implement a mock service because of the following reasons: We didn’t have a that could be used for running tests only. dedicated server Our existing development servers had their and often changed. state updated A server could be off or it could be used to test a new API, and some APIs might be broken at the time. a dedicated server in an up-to-date state would require more time to communicate with the backend team. Supporting The network request execution on a real server, but with mocks, we receive the response almost instantly. takes time We created an entity called that basically contained one function: MockServer func handleRequest(request: NetworkRequest, completion: @escaping RequestCompletion) It accepts the object, and is used for passing the request’s response. NetworkRequest closure In our case, is a simple structure containing all the necessary data to make requests (URL, HTTP method, parameters, body, etc.) and conforms to protocol (to be able to use it in the statement). NetworkRequest Equatable switch This function contains the logic that decides whether the request should return a successful response or error: typealias RequestCompletion = (Data?, WirexAPIError?) -> Void final class MockServer { static let shared = MockServer() private init() { // function that clears local database on app start. I did not include it in the code snippet. self.clearDataIfNeeded() } func handleRequest(request: NetworkRequest, completion: @escaping RequestCompletion) { // Convenience method. Use it for error mocking let sendError: (WirexAPIError) -> Void = { error in self.processResponse(request: request, data: nil, error: error, completion: completion) } // Convenience method. Use it to mock response. Pass nil if only 200 is needed let sendSuccess: (Data?) -> Void = { data in self.processResponse(request: request, data: data, error: nil, completion: completion) } switch request { case .postSignIn(): if !isDeviceConfirmed { sendError(WirexAPIError.confirmSMSError) } else { sendSuccess(nil) } // all other cases default: sendSuccess(nil) // This way we detect the requests, we forgot to mock or mocked wrong while writing ui test print("Here request url, body and any needed info is printed") } } private func processResponse(request: NetworkRequest, data: Data?, error: WirexAPIError?, completion: @escaping RequestCompletion) { if let data = data, let onSuccess = request.onSuccess, error == nil { onSuccess(data, { completion(nil, $0) }) } else { completion(data, error) } } } Mocked data is passed like this . The mock itself is just converted to the data JSON string: sendSuccess(ConfirmLoginMock.response) class ConfirmLoginMock { static let response = """ { "token": "mockedToken", "expires_at": "2021-03-19T16:38:13.3490000+00:00" } """.data(using: .utf8)! } All that is left to do is to inject the function into the app’s network layer. In our project, we have an entity called NetworkManager that has a single point for all incoming requests. It accepts the same parameters as the above-described function: handleRequest func runRequest(request: NetworkRequest, completion: @escaping RequestCompletion) { #if DEBUG if isRunningUITests { MockServer.shared.handleRequest(request: request, completion: completion) return } #endif // Here goes the code that makes actual API request and handles response } Any request that will be passed to it will be either mocked or sent to an actual server. We use a constant to detect whether to run tests or not. And since we’re not able to pass the data between the main project and UI tests directly (UI tests are run in isolation), we need to use the of the app. isRunningUITests launch arguments We can do it in two steps. The first is to set an argument before the start of the UI test: // Every UI test can set up it's own launchArguments if needed override func setUp() { super.setUp() continueAfterFailure = false app = XCUIApplication() app.launchArguments.append("UITests") } The second step is getting this argument somewhere in the main project. let isRunningUITests = ProcessInfo.processInfo.arguments.contains("UITests") Lessons we Learned Make sure you clear your local persistent storage, User Defaults, local caches, or any temporary data that influences your app behavior before the UI test is run. Every test needs to start from the same app state because it may still succeed when run independently but may fail when run together with the rest of the tests if they share the same state. Double-check your mock data, it may save you a lot of time and effort. We used the raw JSON data for the mocks. We always checked that it was a valid JSON before passing it into the codebase. Keep your tests code clean. Although it’s not a production code, you may need to return to it in the future — if it’s a mess, it will be hard to work on.