3 minute read

Painless BDD on iOS with XCTContext

Do you like BDD?

To be honest, I don’t. I do like the idea behind it, but the implementation usually way too complicates things for no reason and makes it super painful to develop and support the automated testing frameworks.

People usually say it’s there to allow non-technical folks to read and write the automated tests on their own. Really? Okay. If something breaks under the hood, these folks will be the first ones to complain. And, yeah, they will be right. But it’s not the point. The point is that it’s not a good fit for automated tests.

There is one thing though that I really like about BDD. It makes the test scenario look like a story, which side effect is that everyone can understand it. GIVEN anything => WHEN something => THEN nothing, you know?

What if XCTest could offer something like this out of the box? It would be a win-win, wouldn’t it? Well, XCTest does offer this. It’s called XCTContext.

Let’s play with the following tiny test scenario:

GIVEN user opens chat
WHEN user sends message
THEN user observes new message

Then

I’ve seen multiple times on various projects that developers usually use comments to mimic BDD, something like:

import XCTest

class SampleTestCase: XCTestCase {
    func testExample() {
        func testExample() {
            let message = "test"

            // GIVEN user opens chat
            user
                .login()
                .tapOnChatButton()

                // WHEN user sends message
                .typeText(message)
                .tapOnSendButton()

                // THEN user observes new message
                .assertMessageContent(message)
                .assertMessageAuthor(user)
        }
    }
}

And this is not too bad if you want to avoid a complex BDD framework under the hood while still keeping the tests kinda readable.

However, the test report looks quite messy on success:

Test result of success

And a bit confusing on failure:

Test result of failure

Now

I’ve already mentioned that XCTContext might help, so it will transform our test case into something like this:

func testExample() {
    let message = "test"

    GIVEN("user opens chat") {
        user
            .login()
            .tapOnChatButton()
    }
    WHEN("user sends message") {
        user
            .typeText(message)
            .tapOnSendButton()
    }
    THEN("user observes new message") {
        user
            .assertMessageContent(message)
            .assertMessageAuthor(user)
    }
}

It looks so much better — documentation as a code, isn’t it? It also helps to understand the test case structure either when you are developing, reviewing, or editing the automated test.

Besides, just look at this compact test report on success:

Test result of success

And on failure:

Test result of failure Test result of failure

Multiply this by the number of steps a real test case usually has, and the difference in reports between the two approaches will be hilarious.

Under the hood

To make our beautiful «BDD framework» work we just need to extend the XCTest class and add a few new functions:

import XCTest

public extension XCTest {
    func GIVEN(_ name: String, actionStep: () -> Void) {
        step("GIVEN \(name)", step: actionStep)
    }

    func WHEN(_ name: String, actionStep: () -> Void) {
        step("WHEN \(name)", step: actionStep)
    }

    func THEN(_ name: String, actionStep: () -> Void) {
        step("THEN \(name)", step: actionStep)
    }

    func AND(_ name: String, actionStep: () -> Void) {
        step("AND \(name)", step: actionStep)
    }

    private func step(_ name: String, step: () -> Void) {
        XCTContext.runActivity(named: name) { _ in
            step()
        }
    }
}

And that’s it! We’ve literally just wrapped up the XCTContext.runActivity() into a small function step() and created the behaviour-driven keywords that we can use in our tests.

You may notice that I’ve added AND as a keyword as well. This is just a personal preference, but I like to use it in test scenarios that have more than 3 steps. To make this clear, just imagine our initial scenario with AND:

GIVEN user logs in
AND user opens chat
WHEN user types message
AND user sends message
THEN user observes new message
AND message belongs to user

Conclusion

I prefer to treat automated tests as documentation. And if I call it documentation, I have to keep it readable. Ideally without either outdated comments or BDD colliders.

XCTContext solves this issue perfectly. Just give it a try, and you’ll see how much easier it is to read, understand and maintain the automated tests.

Updated: