BDD Testing in Swift with Quick and Nimble

Quick and Nimble are the most popular BDD testing libraries for Swift.

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 '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 AsyncSpec — async/await in it blocks
  • Custom matchers with meaningful failure messages improve test readability
  • Shared examples reuse behavior specs across multiple types

Read more

Start now free