Painless UI Testing in iOS (Part 3) - Disabling Animations

Table of Contents

Let’s be realistic, UI Testing is a pain. For everybody. And with everybody, I include both Android and iOS developers. The main reason for it is that we’ve all come to this point when one-tenth of the UI Tests fail, randomly, for no definite reason. Could be the CI Virtual Machine which was slower than usual, or a small latency in the emulator… Don’t get me started with execution time… We’ve all been through that.
Well, guess what!

The time has come to make UI Testing great again.

🎬 Hi there, I’m Jean!

Previously on the series Painless UI Testing in iOS, we have walked through Stubbing the Navigation using Coordinators. 🚦
In this episode, we will take a closer look at Disabling Animations! 🎭

Disabling Animations


Animations are a great tool for making the App’s UX feel great, but become a real burden when it comes to UI Testing. In Android, animations can be disabled on the simulator itself but on iOS, it has to be done in the App.

This is not something we can neglect, especially when animations have delays and/or durations: it can slow down the UI tests execution time significantly and even introduce flakiness. 😱
But well, you should know by now that this problem won’t be unanswered! 🧐

The same way we did with Stubbing the Navigation, let’s create an enum. 👍
In it, let’s also add a Bool computed property to toggle the animations. 👨‍🎨

import Foundation

enum AnimationStub: Identifiable {
    case enableAnimations, disableAnimations

    var areAnimationsEnabled: Bool {
        switch self {
        case .enableAnimations:
            return true
        case .disableAnimations:
            return false
        }
    }
}

Looking good! 😎
Now, once more, let’s make it Codable so we can inject it in the ProcessInfo! 💉

import Foundation

extension AnimationStub: Codable {

    enum CodingKeys: String, CodingKey { case areAnimationsEnabled }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        if let areAnimationsEnabled = try? values.decode(Bool.self, forKey: .areAnimationsEnabled) {
            self = areAnimationsEnabled ? .enableAnimations : .disableAnimations
            return
        }

        self = .disableAnimations
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        switch self {
        case .enableAnimations:
            try container.encode(true, forKey: .areAnimationsEnabled)
        case .disableAnimations:
            try container.encode(false, forKey: .areAnimationsEnabled)
        }
    }
}

Nothing new, all clear! ✅
In the AppDelegate, we can now decode the AnimationStub and toggle the animations with it. 👌

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...

    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        if let animationStub = ProcessInfo.processInfo.decode(AnimationStub.self) {
            UIView.setAnimationsEnabled(animationStub.areAnimationsEnabled)
        }
        ...

        return true
    }
    ...
}

You didn’t expect it to be that easy did ya’?! 😜
Last but not least, let’s complete our XCUIApplication extension to include our AnimationStub in the launch environment and we are good to go! 🚀

import XCTest

extension XCUIApplication {

    func launch(_ coordinatorStub: CoordinatorStub, with sessionMock: URLSessionMock = URLSessionMock(), and animationStub: AnimationStub = .disableAnimations) {
        launchEnvironment[CoordinatorStub.identifier] = coordinatorStub.json
        launchEnvironment[URLSessionMock.identifier] = sessionMock.json
        launchEnvironment[AnimationStub.identifier] = animationStub.json
        launch()
    }
}

That’s it! 🎉
Now, by default, animations will be disabled during UI Testing! 💪
No more random flakiness! 🍾

But if for one UI test we suddenly decide to keep the animations enabled, we can simply add the case .enableAnimations in the launch function and we’re all set! 🙌

import XCTest

final class SomethingUITest: XCTestCase {
    private lazy var app = XCUIApplication()

    override func setUp() {
        super.setUp()
        ...
    }

    func testOpenSomething() {
        let something = Something(stuff: "Stuff", ...)
        let sessionMock = URLSessionMock(
            responses: [
                .fetchSomething: [
                    Response(File("something_response", .json)),
                    Response(error: .invalidResponse)
                ]
            ]
        )

        app.launch(.openSomething(something), with: sessionMock, and: .enableAnimations)
        ...
    }
    ...
}

Was it worth it? Did that help you overcome the problems of UI Testing, finishing with Disabling Animations? Are you now feeling more confident about writing UI Tests?

If so, follow me on Twitter, I’ll be happy to answer any of your questions and you’ll be the first one to know when a new article comes out! 👌

If you’d like to see a real-life example of an app doing extensive UI Testing, don’t hesitate to check out my Github repo! 📦

Bye Bye! 👋