SwiftQC is a Swift 6+ property-based testing library designed to unify concepts from QuickCheck, Hedgehog, and modern Swift testing into a single, cohesive framework.
It provides:
- Composable Generators (
Genvia PointFree'sswift-gen) - Pluggable Shrinkers (
Shrinker,Sendable) for minimal counterexamples - An
Arbitraryprotocol (Sendable) tying types (Value: Sendable) to their default(Gen, Shrinker) - A Property Runner (
forAll) with automatic shrinking, seed handling, and seamless Swift Testing issue integration - Stateful testing via
StateModelprotocol andstateful()runner with full sequence shrinking support โ - Parallel testing via
ParallelModelprotocol andparallel()runner (core functionality complete, advanced linearizability features TBD)
Add SwiftQC to your project:
// Package.swift
.package(url: "https://github.com/Aristide021/SwiftQC.git", from: "1.0.0"),Or in Xcode: File โ Add Packagesโฆ and enter the repository URL.
# Clone and run locally (recommended for now)
git clone https://github.com/Aristide021/SwiftQC.git
cd SwiftQC
swift run SwiftQCCLI --help
swift run SwiftQCCLI run --count 100
swift run SwiftQCCLI interactiveNote: We recommend using the library directly in your projects rather than the CLI for production use.
๐ See INSTALL.md for detailed installation options and troubleshooting.
Import the library and write a property test in your test suite:
import SwiftQC
import Testing // Or XCTest
// In your test file (e.g., MyLibraryTests.swift)
@Test // Using Swift Testing syntax
func additionIsCommutative() async {
// Test that integer addition is commutative
await forAll("Int addition is commutative") { (a: Int, b: Int) in
#expect(a + b == b + a)
// Or using XCTest:
// XCTAssertEqual(a + b, b + a)
}
}
// --- Or using XCTest ---
// class MyLibraryTests: XCTestCase {
// func testAdditionIsCommutative() async {
// await forAll("Int addition is commutative") { (a: Int, b: Int) in
// XCTAssertEqual(a + b, b + a)
// }
// }
// }Run your tests using the standard command:
swift testSwiftQC's forAll function will automatically generate random inputs, run your property, shrink failures to minimal examples, and report results (integrating with Swift Testing's issue system if used).
SwiftQC provides Arbitrary conformance for many standard Swift types out of the box:
- Numeric:
Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64,Float,Double,CGFloat,Decimal - Text:
Character,String,Unicode.Scalar - Boolean:
Bool - Data:
Data,UUID - Collections:
Array<T>,Dictionary<K, V>(viaArbitraryDictionary<K, V>),Set<T>,Optional<T>,Result<Success, Failure> - Time:
Date(with reasonable ranges)
SwiftQC provides several overloads of the forAll function for different testing scenarios.
For properties involving a single type conforming to Arbitrary:
import SwiftQC
import Testing
@Test
func stringReversalIdentity() async {
await forAll("String reversal identity") { (s: String) in
let reversed = String(s.reversed())
let backToOriginal = String(reversed.reversed())
#expect(backToOriginal == s)
}
}For properties involving multiple Arbitrary types, provide the types (.self) after the description:
@Test
func stringConcatenationLength() async {
await forAll(
"String concatenation preserves length",
String.self, String.self
) { (s1: String, s2: String) in
let combined = s1 + s2
#expect(combined.count == s1.count + s2.count)
}
}A specialized overload handles Dictionary properties. You need to provide the Arbitrary types for the Key and Value. The Key's Value must be Hashable.
@Test
func dictionaryMerging() async {
await forAll(
"Dictionary merging combines entries",
String.self, // Key type
Int.self, // Value type
forDictionary: true
) { (dict: [String: Int]) in
let emptyDict: [String: Int] = [:]
let merged = dict.merging(emptyDict) { (current, _) in current }
#expect(merged == dict)
}
}You can make any type Arbitrary by implementing the protocol:
struct Point: Arbitrary, Sendable {
let x: Int
let y: Int
typealias Value = Point
static var gen: Gen<Point> {
zip(Int.gen, Int.gen).map { Point(x: $0, y: $1) }
}
static var shrinker: any Shrinker<Point> {
Shrinkers.map(
from: Shrinkers.tuple(Int.shrinker, Int.shrinker),
to: { Point(x: $0.0, y: $0.1) },
from: { ($0.x, $0.y) }
)
}
}See Arbitrary.md for detailed instructions and examples.
Explore hands-on examples in the Examples/ directory:
- BasicUsage - Property testing fundamentals, custom types, shrinking
- StatefulExample - Testing state machines, command sequences
- ParallelExample - Concurrent testing, race condition detection
Each example is a complete Swift package you can build and run:
cd Examples/BasicUsage && swift testExplore the Docs/ directory for comprehensive documentation:
- GettingStarted.md - A complete guide to using SwiftQC
- Arbitrary.md - Creating custom
Arbitrarytypes - Generators.md - Working with and composing generators
- Shrinkers.md - Understanding shrinking for minimal counterexamples
- Stateful.md - Testing stateful systems
- Parallel.md - Testing concurrent systems
- Integration.md - Integration with Swift Testing
SwiftQC is released under the Apache License 2.0. See LICENSE for details.