Property-based Testing in Swift: When Regular Tests Fall Short
Have you ever written a unit test that passes with flying colors, only to have the app crash on real data? Sounds familiar, right? Classic example-based tests (where we check a specific input and output) are good, but they have a fundamental flaw: they only check what we ourselves thought of. But what if a bug lives on the boundary of valid values, or in a combination that simply didn't occur to us?
That's where property-based testing in Swift comes into play. This is an approach where you describe not specific cases, but properties that should hold true for any input data. A special framework (like SwiftCheck) then generates hundreds of random values and checks whether your property is violated. If a bug is found, the framework "shrinks" the input data — and you get the simplest example that breaks everything.
In this article, we'll break down how to implement property-based testing Swift in your project, write real code examples, and see what bugs can be caught with this approach. Let's go!
What is Property-based Testing and Why Do You Need It?
Imagine you're testing a function that sorts an array. Instead of writing 10 tests with different arrays, you say: "for any array, after sorting, the length doesn't change, and each element is less than or equal to the next one." That's a property — an invariant that must always hold.
Property-based testing in Swift is ideal for:
- Checking mathematical functions (addition, division, rounding);
- Testing parsers and serializers (serialization → deserialization should return the original);
- Verifying business logic with many combinations;
- Finding regressions at boundary values.
The main bonus: you find bugs whose existence you didn't even suspect. And that, you'll agree, is cooler than confirming the obvious.
SwiftCheck — The Main Tool for Property-based Testing in Swift
In the Swift world, the most popular framework for this approach is SwiftCheck. It ports ideas from Haskell (the QuickCheck library) and integrates perfectly with XCTest.
To get started, add the dependency to your Package.swift:
// swift-tools-version:5.9import PackageDescription
let package = Package( name: "MyProject", dependencies: [ .package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.12.0") ], targets: [ .testTarget( name: "MyProjectTests", dependencies: ["SwiftCheck"]), ])Now you can write tests. Let's break down the basic syntax.
Your First Property-based Test: Checking Addition
Let's write a simple property: "addition is associative". For any integers, a + b + c should equal (a + b) + c.
import XCTestimport SwiftCheck
class MathPropertiesTests: XCTestCase { func testAdditionIsAssociative() { property("Addition is associative") <- forAll { (a: Int, b: Int, c: Int) in return (a + b) + c == a + (b + c) } }}Run the test — it will execute 100 random combinations (by default). If even one fails, SwiftCheck will show the specific values. Note: we didn't specify a single concrete number! Property-based testing Swift generates the data itself.
Checking Roundtrip (Inverse Transformation)
This is the most common pattern. Let's say we have a structure that serializes to JSON. The property: "after serialization and deserialization, we get the same object."