Quick/Nimble
A Matcher Framework for Swift and Objective-C
Healthy across the board
Permissive license, no critical CVEs, actively maintained — safe to depend on.
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
No critical CVEs, sane security posture — runnable as-is.
- ✓Last commit 5d ago
- ✓15 active contributors
- ✓Apache-2.0 licensed
Show 3 more →Show less
- ✓CI configured
- ✓Tests present
- ⚠Concentrated ownership — top contributor handles 67% of recent commits
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Healthy" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/quick/nimble)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/quick/nimble on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: Quick/Nimble
Generated by RepoPilot · 2026-05-10 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/Quick/Nimble shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
GO — Healthy across the board
- Last commit 5d ago
- 15 active contributors
- Apache-2.0 licensed
- CI configured
- Tests present
- ⚠ Concentrated ownership — top contributor handles 67% of recent commits
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live Quick/Nimble
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/Quick/Nimble.
What it runs against: a local clone of Quick/Nimble — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in Quick/Nimble | Confirms the artifact applies here, not a fork |
| 2 | License is still Apache-2.0 | Catches relicense before you depend on it |
| 3 | Default branch main exists | Catches branch renames |
| 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code |
| 5 | Last commit ≤ 35 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of Quick/Nimble. If you don't
# have one yet, run these first:
#
# git clone https://github.com/Quick/Nimble.git
# cd Nimble
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of Quick/Nimble and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "Quick/Nimble(\\.git)?\\b" \\
&& ok "origin remote is Quick/Nimble" \\
|| miss "origin remote is not Quick/Nimble (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(Apache-2\\.0)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"Apache-2\\.0\"" package.json 2>/dev/null) \\
&& ok "license is Apache-2.0" \\
|| miss "license drift — was Apache-2.0 at generation time"
# 3. Default branch
git rev-parse --verify main >/dev/null 2>&1 \\
&& ok "default branch main exists" \\
|| miss "default branch main no longer exists"
# 4. Critical files exist
test -f "Sources/Nimble/DSL.swift" \\
&& ok "Sources/Nimble/DSL.swift" \\
|| miss "missing critical file: Sources/Nimble/DSL.swift"
test -f "Sources/Nimble/Matchers/Matcher.swift" \\
&& ok "Sources/Nimble/Matchers/Matcher.swift" \\
|| miss "missing critical file: Sources/Nimble/Matchers/Matcher.swift"
test -f "Sources/Nimble/Expectation.swift" \\
&& ok "Sources/Nimble/Expectation.swift" \\
|| miss "missing critical file: Sources/Nimble/Expectation.swift"
test -f "Sources/Nimble/Expression.swift" \\
&& ok "Sources/Nimble/Expression.swift" \\
|| miss "missing critical file: Sources/Nimble/Expression.swift"
test -f "Sources/Nimble/Adapters/AssertionDispatcher.swift" \\
&& ok "Sources/Nimble/Adapters/AssertionDispatcher.swift" \\
|| miss "missing critical file: Sources/Nimble/Adapters/AssertionDispatcher.swift"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 35 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~5d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/Quick/Nimble"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
Nimble is a matcher framework for Swift and Objective-C that enables expressive, English-like assertion syntax in unit tests (e.g., expect(1 + 1).to(equal(2)) or expect(ocean.isClean).toEventually(beTruthy())). It abstracts assertion failures across multiple test frameworks (XCTest, Swift Testing) via an adapter pattern and provides fluent chainable matchers with detailed failure messages. Core capability: composable, readable test assertions with async/await support. Single-target Swift Package with Sources/Nimble/ as the main module. Architecture uses a pluggable adapter pattern: Sources/Nimble/Adapters/ contains AdapterProtocols.swift defining interfaces, with concrete implementations (NimbleXCTestHandler.swift, NimbleSwiftTestingHandler.swift, AssertionRecorder.swift) enabling test framework agnosticity. Core DSL lives in DSL.swift, DSL+AsyncAwait.swift, DSL+Require.swift with Expression/Expectation/AsyncExpression types. Matchers are separate concerns from the assertion dispatcher.
👥Who it's for
Swift and Objective-C developers writing unit tests who want readable, maintainable test code without verbose assertion boilerplate. Particularly used by teams already using Quick (its companion framework) or those seeking BDD-style test syntax. Contributors are primarily Quick/Nimble ecosystem maintainers and Swift testing practitioners.
🌱Maturity & risk
Production-ready and actively maintained. The codebase is 631KB of Swift (major portion) with comprehensive CI/CD via GitHub Actions (8 distinct workflow files including Carthage, SwiftPM, Xcode, and WebAssembly support). Multi-platform support (iOS, macOS, tvOS, Linux, WebAssembly) with CocoaPods/Carthage/SPM integration indicates stable API. Recent workflows for documentation generation and SwiftLint enforcement suggest active development standards.
Standard open source risks apply.
Active areas of work
Active maintenance with recent workflow updates (WASM support added via wasm.yml, SwiftLint integration via swiftlint.yml, documentation automation). The codebase supports Swift 5.7+ (Package@swift-5.9.swift suggests modern toolchain targets). No breaking changes indicated in file structure, suggesting API stability focus. Documentation catalog active at Sources/Nimble/Nimble.docc.
🚀Get running
git clone https://github.com/Quick/Nimble.git
cd Nimble
swift build
swift test
For Xcode: open Nimble.xcodeproj and select the Nimble scheme. For SPM integration into your project, add .package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0") to Package.swift and link to test targets only.
Daily commands:
Development: swift build compiles the framework. Testing: swift test runs the test suite. Xcode workflow: open Nimble.xcodeproj, select Nimble scheme, then Product > Build (⌘B) or Test (⌘U). CI validation runs via GitHub Actions on push (see .github/workflows/ci-xcode.yml, ci-swiftpm.yml).
🗺️Map of the codebase
Sources/Nimble/DSL.swift— Core DSL entry point definingexpect()function and primary assertion API that all users interact with.Sources/Nimble/Matchers/Matcher.swift— Abstract matcher protocol defining the contract all 50+ matchers must implement; fundamental to the framework's extensibility.Sources/Nimble/Expectation.swift— Core expectation container that evaluates expressions against matchers and delegates assertion results to the test framework.Sources/Nimble/Expression.swift— Lazy expression wrapper that defers evaluation until match time; enables both sync and async expression handling.Sources/Nimble/Adapters/AssertionDispatcher.swift— Test framework adapter that routes assertion results to XCTest, Swift Testing, or other test runners; critical integration point.Sources/Nimble/Adapters/NimbleEnvironment.swift— Global configuration and test framework detection system that bootstraps the correct assertion handler at runtime.Sources/Nimble/FailureMessage.swift— Failure message formatting engine that constructs readable assertion failure output; directly affects user debugging experience.
🛠️How to make changes
Add a new built-in matcher
- Create new Swift file in Sources/Nimble/Matchers/ following naming convention BeSomething.swift or SomethingMatcher.swift (
Sources/Nimble/Matchers/Equal.swift) - Implement Matcher<T> protocol with
matches()andfailureMessage()methods; study Equal.swift or BeCloseTo.swift as templates (Sources/Nimble/Matchers/Matcher.swift) - Create top-level function in the same file (e.g.,
func equal<T>(_ value: T) -> Matcher<T>) returning your matcher instance (Sources/Nimble/Matchers/Equal.swift) - Add documentation (.md file) under Sources/Nimble/Nimble.docc/Matchers/ grouping with related matchers (Comparisons, Collections, Equivalence, etc.) (
Sources/Nimble/Nimble.docc/Matchers/Equivalence.md) - Write tests in Tests/ directory (not shown but follow XCTest convention); ensure matcher negation is tested via MatcherProtocols (
Sources/Nimble/Matchers/MatcherProtocols.swift)
Add support for a new test framework (beyond XCTest/Swift Testing)
- Create new handler file in Sources/Nimble/Adapters/ named Nimble[FrameworkName]Handler.swift (
Sources/Nimble/Adapters/NimbleXCTestHandler.swift) - Implement AssertionHandler protocol (from AdapterProtocols.swift) with
recordAssertionFailure()method that calls framework-specific assertion functions (Sources/Nimble/Adapters/AdapterProtocols.swift) - Register handler in NimbleEnvironment.swift in the framework detection logic (updateEnvironment function) (
Sources/Nimble/Adapters/NimbleEnvironment.swift) - Add import guards and compiler directives if needed; update AssertionDispatcher.swift to route to your handler (
Sources/Nimble/Adapters/AssertionDispatcher.swift)
Add an async/concurrent matcher variant
- Create AsyncSomethingMatcher.swift file in Sources/Nimble/Matchers/ or add async logic to existing matcher (
Sources/Nimble/Matchers/AsyncAllPass.swift) - Implement AsyncMatcher<T> protocol instead of Matcher<T>; define
asyncMatches()as async function returning MatcherResult (Sources/Nimble/Matchers/AsyncMatcher.swift) - Ensure integration with AsyncExpression by returning matcher instance from top-level function (
Sources/Nimble/AsyncExpression.swift) - Test via DSL+AsyncAwait.swift patterns (e.g.,
await expect(...).to(...)with async matcher) (Sources/Nimble/DSL+AsyncAwait.swift)
🔧Why these technologies
- Swift generics and protocol-oriented design — Enables type-safe matchers for any Equatable/Comparable type while maintaining clean public API; allows matcher composition without runtime reflection
- Lazy evaluation (Expression closures) — Defers value computation until match time; critical for
.toEventually()polling and async/concurrent assertions without blocking test setup - Async/await (AsyncExpression, DSL+AsyncAwait) — Native Swift concurrency support for async function testing; avoids callback hell and integrates naturally with modern async code
- Multiple package managers (SPM, CocoaPods, Carthage) — Maximizes adoption across iOS/macOS ecosystem; different teams use different dependency managers
- Framework-agnostic adapter pattern (XCTest + Swift Testing handlers) — Decouples Nimble's core from specific test framework; allows future framework support without core rewrites
⚖️Trade-offs already made
-
Lazy evaluation via Expression closures instead of eager value capture
- Why: Enables
.toEventually()polling and async support without blocking; separates expression definition from assertion time - Consequence: Slightly more complex API (closures
{ ... }required); harder to understand for beginners; cannot inspect expression code for better error messages
- Why: Enables
-
Protocol-based matchers instead of enum-based DSL
- Why:
- Consequence: undefined
🪤Traps & gotchas
No external dependencies means no version conflicts, but watch Swift toolchain compatibility—Package@swift-5.9.swift exists alongside Package.swift suggesting conditional compilation or dual versioning. The adapter pattern hides test framework detection: if both XCTest and Swift Testing are available, the dispatcher must choose consistently (check NimbleEnvironment.swift for runtime detection logic). Async/await helpers require iOS 13+/Swift 5.5+, not a true blocker but limits legacy project use. Documentation generation (documentation.yml) requires docc tooling—ensure Swift 5.9+ for building docs locally.
🏗️Architecture
💡Concepts to learn
- Matcher Protocol & Composability — Nimble's core abstraction—matchers are composable functions that encapsulate assertion logic; understanding the Matcher<T> protocol and how
to()chains them is essential to extending the framework. - Adapter Pattern for Test Framework Abstraction — Nimble uses adapters (AdapterProtocols.swift, NimbleXCTestHandler, NimbleSwiftTestingHandler) to decouple assertions from specific test frameworks; this pattern enables portability across XCTest, Swift Testing, and custom handlers.
- Lazy Expression Evaluation — Expression.swift wraps values without evaluating them immediately; matchers evaluate lazily, enabling detailed failure messages and the
toEventually()polling mechanism without re-executing side effects. - DSL (Domain-Specific Language) & Fluent Builders — Nimble's readable syntax (
expect(x).to(equal(y))) is a DSL implemented via method chaining; understanding how Swift's operator overloading and extension methods enable this makes extending the DSL straightforward. - Async/Await Integration & Polling — DSL+AsyncAwait.swift and
toEventually()implement time-based polling for async assertions without blocking; critical for testing concurrent code and eventual consistency patterns. - Protocol Witness Tables & Generic Constraints — Nimble's type-safe matcher system relies on Swift's protocol witness mechanics and generic constraints (Expression<T>, Matcher<T>) to ensure compile-time safety while supporting any Equatable type.
- Assertion Recording & Fail-Safe Test Isolation — AssertionRecorder.swift captures assertion failures without immediately halting test execution, enabling async contexts to complete and report multiple failures—essential for testing concurrent code without false negatives.
🔗Related repos
Quick/Quick— Companion BDD framework; Nimble is designed as its assertion layer, together they provide RSpec-style test structure and readable expectations.pointfreeco/swift-snapshot-testing— Complementary Swift testing library for snapshot/golden file assertions; often used alongside Nimble in comprehensive test suites.groue/GRDB.swift— Database testing patterns using Nimble matchers extensively for SQLite assertions in iOS; demonstrates real-world Nimble adoption.Alamofire/Alamofire— HTTP networking library frequently tested with Nimble matchers; shows Nimble usage for async API response assertions.pivotal/cedar— Inspiration: the original Objective-C matcher framework that Nimble is based on, demonstrating matcher DSL evolution from Objective-C to Swift.
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add async matcher tests for AsyncAllPass and AsyncMatcher
The Sources/Nimble/Matchers directory contains AsyncAllPass.swift and AsyncMatcher.swift, but there's no visible test coverage for async-specific matcher behavior. Given that the codebase has DSL+AsyncAwait.swift and AsyncExpression.swift, comprehensive async matcher tests would ensure async/await patterns work correctly and help new contributors understand the async testing patterns.
- [ ] Create Tests/NimbleTests/AsyncMatchersTests.swift (or similar structure based on existing test layout)
- [ ] Add tests for AsyncAllPass matcher with various async closure scenarios
- [ ] Add tests for AsyncMatcher protocol implementation with failure cases
- [ ] Add tests for error handling in async matchers
- [ ] Verify tests run in existing CI workflows (ci-xcode.yml, ci-swiftpm.yml)
Add CI workflow for testing Swift 5.9+ Package.swift variant
The repo has Package.swift and Package@swift-5.9.swift (a variant for Swift 5.9+), but there's no dedicated GitHub Actions workflow testing the Swift 5.9+ specific package manifest. This ensures the @swift-5.9 variant builds correctly with newer Swift language features and dependency constraints.
- [ ] Create .github/workflows/ci-swiftpm-5.9.yml workflow file
- [ ] Configure workflow to explicitly test Package@swift-5.9.swift using swift-tools-version 5.9+
- [ ] Run tests and build verification steps similar to ci-swiftpm.yml
- [ ] Add matrix for Swift 5.9, 5.10+ versions if available on runners
- [ ] Update README.md CI badges if appropriate
Complete documentation for Adapters and Protocol-based Testing Architecture
The Sources/Nimble/Adapters directory contains AdapterProtocols.swift, NimbleEnvironment.swift, and multiple test framework adapters (NimbleXCTestHandler.swift, NimbleSwiftTestingHandler.swift), but Sources/Nimble/Nimble.docc likely lacks detailed documentation on how to implement custom assertion adapters or understand the adapter architecture. This is critical for maintainability and allowing framework integrations.
- [ ] Review Sources/Nimble/Adapters/AdapterProtocols.swift and document the main protocols (AssertionHandler, etc.)
- [ ] Create Sources/Nimble/Nimble.docc/Guides/CustomAdapters.md explaining how to implement a custom adapter
- [ ] Document the NimbleEnvironment singleton and its role in adapter selection
- [ ] Add code examples showing XCTest vs Swift Testing handler integration patterns
- [ ] Document the dispatcher pattern used in AssertionDispatcher.swift
🌿Good first issues
- Add missing matcher documentation examples: Sources/Nimble/Matchers/ likely has 50+ matchers, but Nimble.docc may lack comprehensive example snippets for each—audit the .docc catalog against actual matchers and file PRs to add missing examples with real-world use cases.
- Improve error messages for tuple comparison matchers: When
expect((a, b)).to(equal((x, y)))fails, the diagnostic output could decompose which element mismatched—currently likely shows the entire tuple. Enhance ExpectationMessage.swift or a specific tuple matcher with granular failure hints. - Add async/await test coverage gaps: DSL+AsyncAwait.swift and AsyncExpression.swift exist, but review Tests/ for coverage of edge cases like timeout behavior in
toEventually(), cancellation handling, and multiple async expectations in sequence—file PRs to close coverage gaps with concrete test cases.
⭐Top contributors
Click to expand
Top contributors
- @younata — 67 commits
- @dependabot[bot] — 18 commits
- @bnickel — 3 commits
- @CraigSiemens — 1 commits
- @balavor — 1 commits
📝Recent commits
Click to expand
Recent commits
727f75d— Replace use of deprecated Package.init (#1205) (CraigSiemens)035b88a— [v14.0.0] Update podspec (younata)96ac61f— Fix waitUntil, which was broken in the new implementation of polling expectations (#1202) (younata)5a0cc37— Reimplement polling expectations to make them more reliable under heavy system loads (#1199) (younata)cc945f7— [v13.8.0] Update podspec (younata)1ee0178— Do not fail on timeout fortoAlwaysandtoNever(#1192) (bnickel)15106f1— Fix linux cfrunloop build (#1200) (younata)ae0648d— Remove duplicated runloop code (#1193) (bnickel)4cde9c5— Bump rexml from 3.3.9 to 3.4.2 (#1198) (dependabot[bot])0d5588c— Return Never from requireFail (#1194) (bnickel)
🔒Security observations
The Nimble codebase demonstrates a strong security posture overall. As a Swift/Objective-C matcher testing framework, it has no direct injection risks (SQLi, XSS) or exposed sensitive infrastructure. No hardcoded secrets or credentials were identified in the file structure. The repository has good security practices including Dependabot configuration for dependency management. Minor concerns include the need to verify Dockerfile and GitHub Actions workflow configurations for security best practices, and ensuring third-party dependencies are regularly audited. The distributed nature of dependency management (multiple package managers) requires careful coordination but is mitigated by the Dependabot setup.
- Low · Dependency Management Configuration Exposed —
Cartfile.resolved, Package.resolved, Nimble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved. The repository contains Cartfile.resolved and Package.resolved files which are dependency lock files. While these are typically version-controlled, they should be reviewed to ensure no private or vulnerable dependencies are included. The presence of multiple dependency management files (Carthage, CocoaPods, SwiftPM) increases the attack surface. Fix: Review dependency versions regularly for security updates. Consider implementing automated dependency scanning using tools like Dependabot (already configured in .github/dependabot.yml, which is good). Ensure all transitive dependencies are audited. - Low · Docker Test Environment Configuration —
Dockerfile.test. A Dockerfile.test file is present in the repository. While this is a testing utility and not for production, it should be reviewed to ensure it doesn't contain hardcoded credentials, secrets, or insecure base images. Fix: Verify the Dockerfile uses a secure base image (latest stable versions), runs as non-root user, and doesn't contain any hardcoded secrets or sensitive information. Implement image scanning in CI/CD pipeline. - Low · GitHub Actions Workflow Security —
.github/workflows/. Multiple GitHub Actions workflows are configured (.github/workflows/). These workflows have elevated permissions and should be carefully reviewed to prevent supply chain attacks, especially in the release.yml workflow which may have publishing capabilities. Fix: Review all workflow files for: (1) Use of pinned versions for third-party actions, (2) Minimal required permissions using 'permissions' key, (3) Protected branch policies for releases, (4) Secure secret management (no hardcoded credentials), (5) OIDC token usage instead of personal access tokens where possible.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.