Nimble Assertion Framework

repository·main·Indexed 26 days ago

https://github.com/quick/nimble

A powerful assertion framework for Swift and Objective-C that provides a readable, domain-specific language for expressing expected outcomes in tests. It features a wide array of matchers, operator overloading for Swift, polling expectations like `toEventually` for asynchronous values, and support for Swift Concurrency (async/await) via `expecta` and `AsyncMatcher`.

Tokens
15.9K
Snippets
60
Records
75
Agent score
88%

What's inside Nimble

  1. Overview of Nimble testing concepts

    main

    Nimble is a testing framework for verifying the outcomes of Swift or Objective-C expressions. It uses a natural language syntax to define expectations. Key concepts include:

    • Expression: A piece of Swift or Objective-C code (e.g., 1 + 1).
    • Behavior: The result or side effect of an expression (e.g., returning 2).
    • Matcher: A Nimble function that checks an expression's behavior.
    • Expectation: A combination of an expression and a matcher (e.g., expect(1 + 1).to(equal(2))).
    • Polling Expectation: An expectation that is continuously checked until it passes or times out.
    • Requirement: An expectation that must pass before the test continues, typically defined using require.
  2. Understand the difference between XCTest and Nimble assertions

    main

    While Apple's XCTest framework provides standard assertion macros like XCTAssertEqual, it has limitations regarding expressiveness and boilerplate. Nimble is designed to address these drawbacks by providing a more flexible way to express outcomes.

    XCTest Drawbacks:

    • Limited macro variety: It lacks easy assertions for complex checks, such as verifying if a string contains a substring or if a number is less than or equal to another.
    • Boilerplate for temporal checks: It is difficult to check expressions that change over time without writing significant amounts of boilerplate code.
  3. Poll with `require` using eventual matchers

    main

    You can use require with all polling matchers to wait for a condition to be met. If the condition is eventually met, require returns the value. If it fails (e.g., times out), it throws an error. Supported polling methods include:

    • toEventually
    • eventuallyTo
    • toEventuallyNot
    • toNotEventually
    • toNever
    • neverTo
    • toAlways
    • alwaysTo
  4. Wait for a callback using `waitUntil`

    main

    Use waitUntil to pause a test until a provided callback is executed. The callback receives a done closure that must be called to signal completion. You can optionally specify a timeout parameter.

    Note: waitUntil triggers its timeout code on the main thread. Avoid blocking the main thread (e.g., via sleep() or synchronous IO) as it will prevent the run loop from continuing and cause test pollution.

    // Basic usage
    waitUntil { done in
        ocean.goFish { success in
            expect(success).to(beTrue())
            done()
        }
    }
    
    // With custom timeout
    waitUntil(timeout: .seconds(10)) { done in
        ocean.goFish { success in
            expect(success).to(beTrue())
            done()
        }
    }
  5. Configure global PollingDefaults with XCTest

    main

    To set global polling defaults in a pure XCTest environment, implement an XCTestObserver and register it via XCTestObservationCenter.

    1. Set the NSPrincipalClass key in your test bundle's Info.plist to a class that implements init().
    2. In that class's init(), register your observer.
    3. In the observer's testBundleWillStart(_:) method, set the Nimble.PollingDefaults values.
    // TestSetup.swift
    import XCTest
    import Nimble
    
    @objc
    class TestSetup: NSObject {
        override init() {
            XCTestObservationCenter.shared.register(PollingConfigurationTestObserver())
        }
    }
    
    class PollingConfigurationTestObserver: NSObject, XCTestObserver {
        func testBundleWillStart(_ testBundle: Bundle) {
            Nimble.PollingDefaults.timeout = .seconds(5)
            Nimble.PollingDefaults.pollInterval = .milliseconds(100)
        }
    }
  6. Use Nimble in Objective-C

    main

    Nimble supports Objective-C, but requires that all parameters passed to expect and matcher functions (like equal) are Objective-C objects or types that can be converted to NSObject equivalents.

    Supported automatic conversions include:

    • C Numeric types $\rightarrow$ NSNumber *
    • NSRange $\rightarrow$ NSValue *
    • char * $\rightarrow$ NSString *

    Common matchers that support these conversions include equal, beGreaterThan, beGreaterThanOrEqual, beLessThan, beLessThanOrEqual, beCloseTo, beTrue, beFalse, beTruthy, beFalsy, and haveCount.

    @import Nimble;
    
    // Using boxed numbers
    expect(@(1 + 1)).to(equal(@2));
    expect(2).to(equal(2));
    
    // Using strings
    expect(@"Hello world").to(contain(@"world"));
    expect("Hello world").to(equal("Hello world"));
    
    // Using NSRange
    expect(NSMakeRange(1, 10)).to(equal(NSMakeRange(1, 10)));
  7. Use Polling Expectations to verify asynchronous values

    main

    Nimble provides four forms of polling expectations to test values that change over time. Use these when you need to wait for a condition to be met or to ensure a condition remains true/false throughout a timeout period.

    Polling formPass DurationExpected Matcher Result
    toEventuallyUntil passto match
    toEventuallyNot / toNotEventuallyUntil passto not match
    toAlways / alwaysToUntil failto match
    toNever / neverToUntil failto not match

    Warning: Do not confuse toEventuallyNot with toNever. toEventuallyNot passes as soon as the matcher fails once. toNever continuously polls for the entire timeout duration to ensure the matcher never succeeds.

  8. Configure global PollingDefaults with Quick

    main

    If using the Quick framework, the best way to set global polling defaults is to create a QuickConfiguration subclass and set the values in the configure(_:) method.

    import Quick
    import Nimble
    
    class PollingConfiguration: QuickConfiguration {
        override class func configure(_ configuration: QCKConfiguration) {
            Nimble.PollingDefaults.timeout = .seconds(5)
            Nimble.PollingDefaults.pollInterval = .milliseconds(100)
        }
    }
  9. Test for raised exceptions with lazy evaluation

    main

    Nimble evaluates the expression passed to expect lazily, allowing you to test if an expression raises an exception.

    • Swift: Pass a closure { ... } to expect.
    • Objective-C: Use the expectAction macro for expressions with no return value.

    You can customize the raiseException matcher to check for specific names, reasons, or user info.

  10. Perform custom validation with closures

    main

    You can perform custom validation by passing a closure to expect. The closure must return a result indicating whether the validation succeeded or failed.

    • Return .succeeded to indicate the validation passed.
    • Return .failed(reason: "...") to indicate the validation failed. The string provided in the reason will be displayed in the test failure message.

    Use .to(succeed()) to assert that the closure returns .succeeded, or .notTo(succeed()) to assert that it returns .failed.

    // passes if .succeeded is returned from the closure
    expect {
        guard case .enumCaseWithAssociatedValueThatIDontCareAbout = actual else {
            return .failed(reason: "wrong enum case")
        }
    
        return .succeeded
    }.to(succeed())
    
    // passes if .failed is returned from the closure
    expect {
        guard case .enumCaseWithAssociatedValueThatIDontCareAbout = actual else {
            return .failed(reason: "wrong enum case")
        }
    
        return .succeeded
    }.notTo(succeed())
  11. Use Swift Concurrency (Async/Await) with Nimble

    main

    Nimble supports awaiting async functions before passing their results to matchers. To use this, you must execute your tests in an async context.

    • XCTest: Mark your test function with async.
    • Quick 6: All tests are executed in an async context.
    • Quick 7+: Only tests in an AsyncSpec subclass are executed in an async context.

    To avoid compiler errors when using async expressions, use the expecta function instead of the standard expect function, as expect does not support autoclosures for async expressions.

  12. Expose Swift matchers to Objective-C

    main

    To use a Swift matcher in Objective-C, extend NMBMatcher with a @objc class method that wraps the Swift matcher using .toObjectiveC().

    1. Extend NMBMatcher with a class method.
    2. Inside the method, return an NMBMatcher closure that evaluates the Swift matcher.
    3. (Optional) Define a C function to provide a cleaner syntax in Objective-C.
    // Swift
    extension NMBMatcher {
        @objc public class func beNilMatcher() -> NMBMatcher {
            return NMBMatcher { actualExpression in
                return try beNil().satisfies(actualExpression).toObjectiveC()
            }
        }
    }
    
    // Objective-C usage
    expect(actual).to([NMBMatcher beNilMatcher]());