BDD Testing in Swift with Quick and Nimble
Quick and Nimble are the most popular BDD testing libraries for Swift.
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 pointNil 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]+$")) // regexCollections
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:MyAppTestsProduction 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/itBDD structure; Nimble provides expressive matchers beforeEach/afterEachscoped to their containing block — no global state contaminationcontextis an alias fordescribe— use it to describe preconditions- Async support via
AsyncSpec—async/awaitinitblocks - Custom matchers with meaningful failure messages improve test readability
- Shared examples reuse behavior specs across multiple types