BDD Testing in Swift with Quick and Nimble

BDD Testing in Swift with Quick and Nimble

Quick and Nimble are the most popular BDD testing libraries for Swift. Quick provides a describe/it DSL for organizing tests; Nimble provides expressive matchers. Together they make test intent clearer than raw XCTest and scale better for complex domain logic.

Installation

Swift Package Manager

// Package.swift or Xcode Package Dependencies
.package(url: "https://github.com/Quick/Quick.git", from: "7.0.0"),
.package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"),

Add Quick and Nimble to your test target.

A Quick Spec

import Quick
import Nimble

final class CalculatorSpec: QuickSpec {

    override class func spec() {
        describe("Calculator") {
            var calculator: Calculator!

            beforeEach {
                calculator = Calculator()
            }

            describe("add") {
                it("returns the sum of two numbers") {
                    expect(calculator.add(2, 3)).to(equal(5))
                }

                it("handles negative numbers") {
                    expect(calculator.add(-1, 1)).to(equal(0))
                }
            }

            describe("divide") {
                it("returns the quotient") {
                    expect(try calculator.divide(10, by: 2)).to(equal(5))
                }

                it("throws on division by zero") {
                    expect { try calculator.divide(10, by: 0) }
                        .to(throwError(CalculatorError.divisionByZero))
                }
            }
        }
    }
}

Tests are organized in describe blocks; individual cases use it. beforeEach runs before each it within its containing describe.

Nimble Matchers

Nimble's matcher library is more expressive than XCTest:

Equality and Comparison

expect(result).to(equal(42))
expect(result).notTo(equal(0))
expect(result).to(beGreaterThan(10))
expect(result).to(beLessThanOrEqualTo(100))
expect(result).to(beCloseTo(3.14, within: 0.01))  // floating point

Nil and Optional

expect(value).to(beNil())
expect(value).notTo(beNil())

Boolean

expect(condition).to(beTrue())
expect(condition).to(beFalse())

Strings

expect(string).to(contain("hello"))
expect(string).to(beginWith("Hello"))
expect(string).to(endWith("world"))
expect(string).to(match("^[A-Z][a-z]+$"))  // regex

Collections

expect(array).to(haveCount(3))
expect(array).to(contain("element"))
expect(array).to(containElementSatisfying { $0.age > 18 })
expect(array).to(beEmpty())
expect(dict).to(haveKey("name"))

Type Checking

expect(value).to(beAKindOf(UIViewController.self))
expect(value).to(beAnInstanceOf(LoginViewController.self))

Errors

expect { try riskyOperation() }.to(throwError())
expect { try riskyOperation() }.to(throwError(MyError.specific))
expect { try safeOperation() }.notTo(throwError())

Organizing Specs with context

context is an alias for describe — use it to describe preconditions:

describe("UserService") {
    var service: UserService!
    var mockRepository: MockUserRepository!

    beforeEach {
        mockRepository = MockUserRepository()
        service = UserService(repository: mockRepository)
    }

    describe("createUser") {

        context("when repository succeeds") {
            beforeEach {
                mockRepository.stubbedCreateResult = User(id: 1, name: "Alice")
            }

            it("returns the created user") {
                let user = try service.createUser(name: "Alice")
                expect(user.name).to(equal("Alice"))
            }

            it("calls repository once") {
                _ = try service.createUser(name: "Alice")
                expect(mockRepository.createCallCount).to(equal(1))
            }
        }

        context("when repository throws") {
            beforeEach {
                mockRepository.stubbedError = RepositoryError.connectionFailed
            }

            it("propagates the error") {
                expect { try service.createUser(name: "Alice") }
                    .to(throwError(RepositoryError.connectionFailed))
            }
        }
    }
}

describe/context/it nesting maps directly to test report output — failures show the full nesting path.

Async Specs

Quick supports async/await specs:

final class AsyncServiceSpec: AsyncSpec {

    override class func spec() {
        describe("AsyncService") {
            it("fetches data asynchronously") {
                let service = AsyncService()
                let result = try await service.fetchData()
                expect(result).notTo(beEmpty())
            }

            it("throws on network error") {
                let service = AsyncService(url: "invalid://url")
                await expect { try await service.fetchData() }
                    .to(throwError())
            }
        }
    }
}

Use AsyncSpec instead of QuickSpec for async test specs.

beforeSuite and afterSuite

One-time setup across all specs:

final class DatabaseSetupSpec: QuickSpec {
    override class func spec() {
        beforeSuite {
            Database.shared.openInMemory()
        }

        afterSuite {
            Database.shared.close()
        }

        // tests...
    }
}

Custom Matchers

Build domain-specific matchers for clearer failure messages:

import Nimble

func beValidEmail() -> Matcher<String> {
    return Matcher { expression in
        let message = ExpectationMessage.expectedTo("be a valid email address")
        guard let value = try expression.evaluate() else {
            return MatcherResult(status: .fail, message: message.appendedBeNilHint())
        }
        let isValid = value.contains("@") && value.contains(".")
        return MatcherResult(bool: isValid, message: message)
    }
}

// Usage
expect("user@example.com").to(beValidEmail())
expect("notanemail").notTo(beValidEmail())

Custom matchers produce readable failure messages:

expected to be a valid email address, got "notanemail"

Shared Examples

Reuse behavior specs across multiple types:

sharedExamples("a valid serializable object") { (sharedExampleContext: @escaping SharedExampleContext) in
    var subject: Serializable!

    beforeEach {
        subject = sharedExampleContext()["subject"] as? Serializable
    }

    it("serializes without throwing") {
        expect { try subject.serialize() }.notTo(throwError())
    }

    it("deserializes to equal value") {
        let data = try! subject.serialize()
        let restored = try! type(of: subject).deserialize(data)
        expect(restored).to(equal(subject))
    }
}

final class UserSpec: QuickSpec {
    override class func spec() {
        describe("User") {
            itBehavesLike("a valid serializable object") {
                ["subject": User(name: "Alice")]
            }
        }
    }
}

Quick vs XCTest

Feature XCTest Quick/Nimble
Organization class + func test* describe/context/it
Setup setUp/tearDown beforeEach/afterEach
Matchers XCTAssertEqual expect().to(equal())
BDD structure No Yes
Xcode integration Native Plugin-based
Async support Native AsyncSpec

Use XCTest for simple unit tests. Add Quick/Nimble when specs are complex enough to benefit from context/describe nesting and richer matchers.

CI Integration

Quick tests run under xcodebuild test exactly like XCTest — no special flags needed:

xcodebuild test \
  -scheme MyApp \
  -destination <span class="hljs-string">'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:MyAppTests

Production Monitoring

Quick/Nimble tests validate behavior against in-process test doubles. For production monitoring of iOS backend APIs and services, HelpMeTest runs continuous tests against live endpoints — no Xcode, no simulators required.

Summary

  • Quick provides describe/context/it BDD structure; Nimble provides expressive matchers
  • beforeEach/afterEach scoped to their containing block — no global state contamination
  • context is an alias for describe — use it to describe preconditions
  • Async support via AsyncSpecasync/await in it blocks
  • Custom matchers with meaningful failure messages improve test readability
  • Shared examples reuse behavior specs across multiple types

Read more