diff --git a/README.md b/README.md index 3f67d16..6afe63d 100644 --- a/README.md +++ b/README.md @@ -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() { @@ -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. diff --git a/Sources/SwiftFixture/Documentation.docc/SwiftFixture.md b/Sources/SwiftFixture/Documentation.docc/SwiftFixture.md index bfb1ff4..435ab34 100644 --- a/Sources/SwiftFixture/Documentation.docc/SwiftFixture.md +++ b/Sources/SwiftFixture/Documentation.docc/SwiftFixture.md @@ -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`` diff --git a/Sources/SwiftFixture/Documentation.docc/Testing With SwiftFixture.md b/Sources/SwiftFixture/Documentation.docc/Testing With SwiftFixture.md index 597fcc4..f2d92d2 100644 --- a/Sources/SwiftFixture/Documentation.docc/Testing With SwiftFixture.md +++ b/Sources/SwiftFixture/Documentation.docc/Testing With SwiftFixture.md @@ -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: @@ -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. @@ -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. diff --git a/Sources/SwiftFixture/FixtureProviding.swift b/Sources/SwiftFixture/FixtureProviding.swift index acd0a27..44ffad2 100644 --- a/Sources/SwiftFixture/FixtureProviding.swift +++ b/Sources/SwiftFixture/FixtureProviding.swift @@ -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 { - 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 { - 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 { - 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 } } }