Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update documentation and tweak behaviour of Array/Set/Dictionary fixtures #14

Merged
merged 3 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ class UserTests: XCTestCase {

## Why?

Every unit test needs a fixture. Put simply, a fixture is just a piece of information that controls the environment of the test. When testing Swift code, types are your fixtures, like `user` in the above example.
Almost every unit test will need a fixture. Put simply, a fixture is just a piece of information that controls the environment of the test. When testing Swift code, instances of your types are your fixtures, like `user` in the above example.

With every unit test written, you will find yourself needing to initialize values more and more. This can start to become repetitive and as your types grow in complexity, it's likely that the initializer argument list also grows. It's not uncommon to eventually find yourself writing something like this:
With every unit test written, you will find yourself needing to initialize values more and more. This can start to become repetitive and as your projects grow in complexity, it's likely that the initializer argument list also grows. It's not uncommon to eventually find yourself writing something like this:

```swift
func testStateForSubscribedUser() {
Expand Down Expand Up @@ -107,7 +107,7 @@ func testStateForUserWithExpiredSubscription() {
}
```

The three _simple_ tests above end up using 90% more lines of code than they really needed and overall things end up pretty noisy. Additionally, making unrelated changes to `User` such as adding a new property require that we go back and add a new default value to each test when that property shouldn't even have been associated with this test in the first place.
The three _simple_ tests above end up using a lot more code than they really needed and overall things end up pretty noisy. Additionally, making unrelated changes to `User` such as adding a new property require that we go back and add a new default value to each instance we initialize when that property shouldn't even have been associated with this test in the first place.

With a few helper methods, you can certainly improve this a bit, but it can be hard to do so consistently when you end up having to write your own helpers. Furthermore, it is also very tempting to do things that influence your production code, such as providing default values on the main initializer.

Expand Down
3 changes: 1 addition & 2 deletions Sources/SwiftFixture/Documentation.docc/SwiftFixture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ SwiftFixture provides a set of tools designed to help make it easy for you to in
- ``FixtureProviding``
- ``ProvideFixture()``

### Misc
### Errors

- ``PreferredFormat``
- ``ResolutionError``
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,19 @@ The ``Fixture`` class has a primary callable interface used to obtain a fixture
```swift
let fixture = Fixture()

let value: Int = try fixture() // Int.random(in:)
try fixture() as Int
// - 5363896279182060614

try fixture() as Date
// ▿ 2008-09-10 18:34:13 +0000
// - timeIntervalSinceReferenceDate: 242764453.45139748

try fixture() as String
// - "1b3e9b17-d79a-4056-8f2b-73112694fa5c"
```

By default, values produced by ``Fixture`` are done so in a non-deterministic way to encourage [constrained non-determinism](https://blog.ploeh.dk/2009/03/05/ConstrainedNon-Determinism/). This is done to help encourage you to rely less on hardcoded values for expected results as you let SwiftFixture handle things for you.

### Registering value providers

By default, providers for common system types (`Int`, `String`, `Bool`, `Date`, `UUID` etc) are provided, but support for your own types can be added by using the ``Fixture/register(_:provideValue:)-7fin6`` method:
Expand All @@ -98,7 +108,14 @@ fixture.register(User.self) { values in
)
}

let value: User = try fixture() // User.init(id:name:createdAt:isActive:)
let value: User = try fixture()
// ▿ User
// ▿ id: 27310087-1F15-4033-B97B-9E6873B48918
// - uuid: "27310087-1F15-4033-B97B-9E6873B48918"
// - name: "1b3e9b17-d79a-4056-8f2b-73112694fa5c"
// ▿ createdAt: 2012-05-24 21:13:02 +0000
// - timeIntervalSinceReferenceDate: 359586782.8698358
// - isActive: false
```

If a type hasn't been registered, the ``ResolutionError/noProviderRegisteredForType(_:)`` error will be thrown instead.
Expand All @@ -122,11 +139,20 @@ The ``FixtureProviding/provideFixture(using:)`` method is used only when a value

### Overriding Values

In the example above, a `User` fixture is created by calling `try fixture(isActive: true)`, but how does this work?
In addition to creating fixtures entirely with non-deterministic placeholder values, it is not uncommon to want to need to control specific properties. From the `testSummaryForActiveUser()` example above, the `isActive` property needs to always be set to `true` when the test runs:

``Fixture`` is a `@dynamicCallable` type which allows for zero or more arguments to be specified which are then passed into the ``ValueProvider`` instance used for resolving fixture values.
```swift
let user: User = try fixture(isActive: true)
// ▿ User
// ▿ id: 27310087-1F15-4033-B97B-9E6873B48918
// - uuid: "27310087-1F15-4033-B97B-9E6873B48918"
// - name: "1b3e9b17-d79a-4056-8f2b-73112694fa5c"
// ▿ createdAt: 2012-05-24 21:13:02 +0000
// - timeIntervalSinceReferenceDate: 359586782.8698358
// - isActive: true
```

At the time of resolution, the call to ``ValueProvider/get(_:)`` uses the optional `label` argument to map a specific fixture argument to the argument used for resolution.
This works because ``Fixture`` allows for a dynamic set of arguments to be specified. If the label of an argument matches the label passed into ``ValueProvider``'s ``ValueProvider/get(_:)`` method when producing a fixture, the argument is used instead of a placeholder value.

It's recommended that you match the label used with the original initializer argument label to prevent confusion. The best way to do this is to use the ``ProvideFixture()`` macro which can provide automatic conformance to the ``FixtureProviding`` protocol for your types.

Expand Down
45 changes: 35 additions & 10 deletions Sources/SwiftFixture/FixtureProviding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,58 @@ public protocol FixtureProviding {
static func provideFixture(using values: ValueProvider) throws -> Self
}

// MARK: - Containers
// MARK: - Conformances

/// `fixture()` support for `Array` types
extension Array: FixtureProviding {
/// Provide a fixture value for use in testing of a given `Array` type.
///
/// - Throws: Any error thrown when resolving a fixture for `Element` apart from ``ResolutionError``.
/// - Returns: If a fixture can be provided for the `Element` type, an array with a single item will be returned.
/// If `Element` cannot be represented as a fixture because it was not registered, an empty array will be returned instead.
public static func provideFixture(using values: ValueProvider) throws -> Array<Element> {
if let value: Element = try? values.get() {
return [value]
} else {
do {
return [try values.get()]
} catch is ResolutionError {
return Array()
} catch {
throw error
}
}
}

/// `fixture()` support for `Set` types
extension Set: FixtureProviding {
/// Provide a fixture value for use in testing of a given `Set` type.
///
/// - Throws: Any error thrown when resolving a fixture for `Element` apart from ``ResolutionError``.
/// - Returns: If a fixture can be provided for the `Element` type, a set with a single item will be returned.
/// If `Element` cannot be represented as a fixture because it was not registered, an empty set will be returned instead.
public static func provideFixture(using values: ValueProvider) throws -> Set<Element> {
if let value: Element = try? values.get() {
return [value]
} else {
do {
return [try values.get()]
} catch is ResolutionError {
return Set()
} catch {
throw error
}
}
}

/// `fixture()` support for `Dictionary` types
extension Dictionary: FixtureProviding {
/// Provide a fixture value for use in testing of a given `Dictionary` type.
///
/// - Throws: Any error thrown when resolving a fixture for `Key` or `Value` apart from ``ResolutionError``.
/// - Returns: If a fixture can be provided for the `Key` and `Value` type, a dictionary with a single entry will be returned.
/// If `Key` or `Value` cannot be represented as a fixture because it was not registered, an empty dictionary will be returned instead.
public static func provideFixture(using values: ValueProvider) throws -> Dictionary<Key, Value> {
if let key: Key = try? values.get(), let value: Value = try? values.get() {
return [key: value]
} else {
do {
return [try values.get(): try values.get()]
} catch is ResolutionError {
return Dictionary()
} catch {
throw error
}
}
}