Skip to content

Commit c60d66d

Browse files
authored
Introduce the require dsl. For when you need the assertion to pass before continuing (#1103)
* Introduce the require-dsl. expect, but it returns the result of the expression As implied, this is basically a copy-paste of the functionality of expect. It's not complete, and it's not well-tested. But it gets the idea across. Future work is cleaning this up, backfilling tests for features (such as the unwrap function), and adding support for polling requirements (i.e. toEventually). Other than functionality, require also files the errorThrown issue type with XCTest, whereas expect files the assertionFailed issue type. This has minor differences that are mostly semantics. Also, in addition to the require dsl, this also adds unwrap, which is a shorthand for `require(...).toNot(beNil())`. * Backfill some tests for requirement Support async, but not yet polling. * CustomNSError is only defined in Foundation * Support polling using require Oh, gosh. This is terrible. I think a cleaner way to do this would be to change MatcherResult to also store the most recent value. But that would be a breaking change, and might require updating existing matchers, so... no. * require should allow you to discard the result Rename requireAsync to requirea - matching the example set forth by expecta. Adds requires, which is requirea, but for specifying SyncRequirement. * Document require in the README Update the table of contents with doctoc. * Remove AssertionHandler.require., require matcher failures will be recorded as test assertion failures, which is what they are anyway. * backfill tests for pollUnwrap Also actually mark the non-polling require dsl as discardableResult * Mark Expectation.onFailure as deprecated in favor of the require dsl * Rename RequirementError to RequireError Fixed test failure on linux * Allow passing a custom error to be thrown by require * Update readme TOC
1 parent ef552dc commit c60d66d

11 files changed

Lines changed: 2004 additions & 11 deletions

Nimble.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,12 @@
125125
857D1849253610A900D8693A /* BeWithin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857D1848253610A900D8693A /* BeWithin.swift */; };
126126
857D184F2536124400D8693A /* BeWithinTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857D184D2536123F00D8693A /* BeWithinTest.swift */; };
127127
8913649429E6925F00AD535E /* utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F14FB63194180C5009F2A08 /* utils.swift */; };
128+
891729D52B1842D6005CC866 /* DSL+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891729D42B1842D6005CC866 /* DSL+Require.swift */; };
129+
891729D72B18431D005CC866 /* Requirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891729D62B18431D005CC866 /* Requirement.swift */; };
128130
891A04712AB0164500B46613 /* AsyncTimerSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891A04702AB0164500B46613 /* AsyncTimerSequence.swift */; };
131+
8922828A2B2833B7002DA355 /* PollingTest+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892282892B2833B7002DA355 /* PollingTest+Require.swift */; };
132+
8922828D2B283818002DA355 /* Polling+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8922828B2B2837E1002DA355 /* Polling+Require.swift */; };
133+
8922828F2B283956002DA355 /* AsyncAwaitTest+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8922828E2B283956002DA355 /* AsyncAwaitTest+Require.swift */; };
129134
892FDF1329D3EA7700523A80 /* AsyncExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892FDF1229D3EA7700523A80 /* AsyncExpression.swift */; };
130135
896962412A5FABD000A7929D /* AsyncAllPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962402A5FABD000A7929D /* AsyncAllPass.swift */; };
131136
8969624A2A5FAD5F00A7929D /* AsyncAllPassTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */; };
@@ -310,7 +315,12 @@
310315
7B5358C11C39155600A23FAA /* ObjCSatisfyAnyOfTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjCSatisfyAnyOfTest.m; sourceTree = "<group>"; };
311316
857D1848253610A900D8693A /* BeWithin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeWithin.swift; sourceTree = "<group>"; };
312317
857D184D2536123F00D8693A /* BeWithinTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeWithinTest.swift; sourceTree = "<group>"; };
318+
891729D42B1842D6005CC866 /* DSL+Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSL+Require.swift"; sourceTree = "<group>"; };
319+
891729D62B18431D005CC866 /* Requirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Requirement.swift; sourceTree = "<group>"; };
313320
891A04702AB0164500B46613 /* AsyncTimerSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTimerSequence.swift; sourceTree = "<group>"; };
321+
892282892B2833B7002DA355 /* PollingTest+Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollingTest+Require.swift"; sourceTree = "<group>"; };
322+
8922828B2B2837E1002DA355 /* Polling+Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Polling+Require.swift"; sourceTree = "<group>"; };
323+
8922828E2B283956002DA355 /* AsyncAwaitTest+Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncAwaitTest+Require.swift"; sourceTree = "<group>"; };
314324
892FDF1229D3EA7700523A80 /* AsyncExpression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncExpression.swift; sourceTree = "<group>"; };
315325
896962402A5FABD000A7929D /* AsyncAllPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPass.swift; sourceTree = "<group>"; };
316326
896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPassTest.swift; sourceTree = "<group>"; };
@@ -448,13 +458,16 @@
448458
892FDF1229D3EA7700523A80 /* AsyncExpression.swift */,
449459
1FD8CD081968AB07008ED995 /* DSL.swift */,
450460
899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */,
461+
891729D42B1842D6005CC866 /* DSL+Require.swift */,
451462
DA9E8C811A414BB9002633C2 /* DSL+Wait.swift */,
452463
1FD8CD091968AB07008ED995 /* Expectation.swift */,
453464
1FD8CD0A1968AB07008ED995 /* Expression.swift */,
454465
1FE661561E6574E20035F243 /* ExpectationMessage.swift */,
455466
1FD8CD0B1968AB07008ED995 /* FailureMessage.swift */,
456467
1F1871E31CA89FB600A34BF2 /* Polling.swift */,
457468
89F5E0852908E655001F9377 /* Polling+AsyncAwait.swift */,
469+
8922828B2B2837E1002DA355 /* Polling+Require.swift */,
470+
891729D62B18431D005CC866 /* Requirement.swift */,
458471
1F1A742D1940169200FFFC47 /* Info.plist */,
459472
1FD8CD0C1968AB07008ED995 /* Matchers */,
460473
1F1A742E1940169200FFFC47 /* Nimble.h */,
@@ -470,10 +483,12 @@
470483
CDC157902511957100EAA480 /* DSLTest.swift */,
471484
89F5E096290C37B8001F9377 /* OnFailureThrowsTest.swift */,
472485
89F5E06C290765BB001F9377 /* PollingTest.swift */,
486+
892282892B2833B7002DA355 /* PollingTest+Require.swift */,
473487
CDBC39B82462EA7D00069677 /* PredicateTest.swift */,
474488
89F5E095290C37B8001F9377 /* StatusTest.swift */,
475489
1F0648D31963AAB2001F9C46 /* SynchronousTest.swift */,
476490
899441EE2902EE4B00C1FAF9 /* AsyncAwaitTest.swift */,
491+
8922828E2B283956002DA355 /* AsyncAwaitTest+Require.swift */,
477492
89C297CB2A911CDA002A143F /* AsyncTimerSequenceTest.swift */,
478493
89C297CD2A92AB34002A143F /* AsyncPromiseTest.swift */,
479494
965B0D0B1B62C06D0005AE66 /* UserDescriptionTest.swift */,
@@ -959,6 +974,7 @@
959974
1FD8CD451968AB07008ED995 /* BeginWith.swift in Sources */,
960975
1FD8CD4B1968AB07008ED995 /* BeIdenticalTo.swift in Sources */,
961976
1FD8CD431968AB07008ED995 /* BeEmpty.swift in Sources */,
977+
891729D52B1842D6005CC866 /* DSL+Require.swift in Sources */,
962978
1F1871D41CA89EEE00A34BF2 /* NMBStringify.m in Sources */,
963979
A8F6B5BD2070186D00FCB5ED /* SatisfyAllOf.swift in Sources */,
964980
1FD8CD531968AB07008ED995 /* BeNil.swift in Sources */,
@@ -968,8 +984,10 @@
968984
1FD8CD351968AB07008ED995 /* DSL.swift in Sources */,
969985
7B5358BF1C38479700A23FAA /* SatisfyAnyOf.swift in Sources */,
970986
896962412A5FABD000A7929D /* AsyncAllPass.swift in Sources */,
987+
891729D72B18431D005CC866 /* Requirement.swift in Sources */,
971988
1FD8CD391968AB07008ED995 /* Expression.swift in Sources */,
972989
891A04712AB0164500B46613 /* AsyncTimerSequence.swift in Sources */,
990+
8922828D2B283818002DA355 /* Polling+Require.swift in Sources */,
973991
89EEF5A52A03293100988224 /* AsyncMatcher.swift in Sources */,
974992
1FD8CD3B1968AB07008ED995 /* FailureMessage.swift in Sources */,
975993
1FA0C3FF1E30B14500623165 /* Matcher.swift in Sources */,
@@ -1038,6 +1056,7 @@
10381056
DDEFAEB51A93CBE6005CA37A /* ObjCAllPassTest.m in Sources */,
10391057
1F4A56801A3B333F009E1637 /* ObjCBeLessThanTest.m in Sources */,
10401058
857D184F2536124400D8693A /* BeWithinTest.swift in Sources */,
1059+
8922828F2B283956002DA355 /* AsyncAwaitTest+Require.swift in Sources */,
10411060
1F0648CD19639F5A001F9C46 /* ObjectWithLazyProperty.swift in Sources */,
10421061
89F5E0A4290C3803001F9377 /* ObjCContainTest.m in Sources */,
10431062
106112C52251E13B000A5848 /* BeResultTest.swift in Sources */,
@@ -1046,6 +1065,7 @@
10461065
89C297CC2A911CDA002A143F /* AsyncTimerSequenceTest.swift in Sources */,
10471066
DD9A9A9019CF43AD00706F49 /* BeIdenticalToObjectTest.swift in Sources */,
10481067
1F4BB8B61DACA0E30048464B /* ThrowAssertionTest.swift in Sources */,
1068+
8922828A2B2833B7002DA355 /* PollingTest+Require.swift in Sources */,
10491069
8913649429E6925F00AD535E /* utils.swift in Sources */,
10501070
89F5E0A2290C37FB001F9377 /* ObjCBeNilTest.m in Sources */,
10511071
1F0648D51963AAB2001F9C46 /* SynchronousTest.swift in Sources */,

README.md

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,23 @@ expect(ocean.isClean).toEventually(beTruthy())
3232
- [Operator Overloads](#operator-overloads)
3333
- [Lazily Computed Values](#lazily-computed-values)
3434
- [C Primitives](#c-primitives)
35-
- [Async/Await in Expectations](#asyncawait-support)
35+
- [Async/Await Support](#asyncawait-support)
36+
- [Async Matchers](#async-matchers)
3637
- [Polling Expectations](#polling-expectations)
38+
- [Using Polling Expectations in Async Tests](#using-polling-expectations-in-async-tests)
39+
- [Verifying a Matcher will Never or Always Match](#verifying-a-matcher-will-never-or-always-match)
40+
- [Waiting for a Callback to be Called](#waiting-for-a-callback-to-be-called)
41+
- [Changing the Timeout and Polling Intervals](#changing-the-timeout-and-polling-intervals)
42+
- [Changing default Timeout and Poll Intervals](#changing-default-timeout-and-poll-intervals)
43+
- [Quick](#quick)
44+
- [XCTest](#xctest)
3745
- [Objective-C Support](#objective-c-support)
3846
- [Disabling Objective-C Shorthand](#disabling-objective-c-shorthand)
47+
- [Using `require` to demand that a matcher pass before continuing](#using-require-to-demand-that-a-matcher-pass-before-continuing)
48+
- [Polling with `require`.](#polling-with-require)
49+
- [Using `require` with Async expressions and Async matchers](#using-require-with-async-expressions-and-async-matchers)
50+
- [Using `unwrap` to replace `require(...).toNot(beNil())`](#using-unwrap-to-replace-requiretonotbenil)
51+
- [Throwing a Custom Error from Require](#throwing-a-custom-error-from-require)
3952
- [Built-in Matcher Functions](#built-in-matcher-functions)
4053
- [Type Checking](#type-checking)
4154
- [Equivalence](#equivalence)
@@ -49,6 +62,8 @@ expect(ocean.isClean).toEventually(beTruthy())
4962
- [Collection Membership](#collection-membership)
5063
- [Strings](#strings)
5164
- [Collection Elements](#collection-elements)
65+
- [Swift](#swift)
66+
- [Objective-C](#objective-c)
5267
- [Collection Count](#collection-count)
5368
- [Notifications](#notifications)
5469
- [Result](#result)
@@ -61,12 +76,15 @@ expect(ocean.isClean).toEventually(beTruthy())
6176
- [Customizing Failure Messages](#customizing-failure-messages)
6277
- [Basic Customization](#basic-customization)
6378
- [Full Customization](#full-customization)
79+
- [Asynchronous Matchers](#asynchronous-matchers)
6480
- [Supporting Objective-C](#supporting-objective-c)
6581
- [Properly Handling `nil` in Objective-C Matchers](#properly-handling-nil-in-objective-c-matchers)
66-
- [Migrating from the Old Matcher API](#migrating-from-the-old-matcher-api)
6782
- [Installing Nimble](#installing-nimble)
6883
- [Installing Nimble as a Submodule](#installing-nimble-as-a-submodule)
6984
- [Installing Nimble via CocoaPods](#installing-nimble-via-cocoapods)
85+
- [Installing Nimble via Swift Package Manager](#installing-nimble-via-swift-package-manager)
86+
- [Xcode](#xcode)
87+
- [Package.Swift](#packageswift)
7088
- [Using Nimble without XCTest](#using-nimble-without-xctest)
7189

7290
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -677,6 +695,89 @@ NMB_expect(^{ return seagull.squawk; }, __FILE__, __LINE__).to(NMB_equal(@"Squee
677695
names that conflict with Nimble functions, such as `expect` or
678696
`equal`. If that's not the case, there's no point in disabling the
679697
shorthand.
698+
699+
# Using `require` to demand that a matcher pass before continuing
700+
701+
Nimble 13.1 added the `require` dsl to complement `expect`. `require`
702+
looks similar to `expect` and works with matchers just like `expect` does. The
703+
difference is that `require` requires that the matcher passes - if the matcher
704+
doesn't pass, then `require` will throw an error. Additionally, if `require`
705+
does pass, then it'll return the result of running the expression.
706+
707+
For example, in testing a function that returns an array, you might need to
708+
first guarantee that there are exactly 3 items in the array before continuing
709+
to assert on it. Instead of writing code that needlessly duplicates an assertion
710+
and a conditional like so:
711+
712+
```swift
713+
let collection = myFunction()
714+
expect(collection).to(haveCount(3))
715+
guard collection.count == 3 else { return }
716+
// ...
717+
```
718+
719+
You can replace that with:
720+
721+
```swift
722+
let collection = try require(myFunction()).to(haveCount(3))
723+
// ...
724+
```
725+
726+
## Polling with `require`.
727+
728+
Because `require` does everything you can do with `expect`, you can also use
729+
`require` to [poll matchers](#polling-expectations) using `toEventually`,
730+
`eventuallyTo`, `toEventuallyNot`, `toNotEventually`, `toNever`, `neverTo`,
731+
`toAlways`, and `alwaysTo`. These work exactly the same as they do when using
732+
`expect`, except that they throw if they fail, and they return the value of the
733+
expression when they pass.
734+
735+
## Using `require` with Async expressions and Async matchers
736+
737+
`require` also works with both async expressions
738+
(`require { await someExpression() }.to(...)`), and async matchers
739+
(`require().to(someAsyncMatcher())`).
740+
741+
Note that to prevent compiler confusion,
742+
you cannot use `require` with async autoclosures. That is,
743+
`require(await someExpression())` will not compile. You can instead either
744+
make the closure explicit (`require { await someExpression() }`), or use the
745+
`requirea` function, which does accept autoclosures.
746+
Similarly, if you ever wish to use the sync version of `require` when the
747+
compiler is trying to force you to use the async version, you can use the
748+
`requires` function, which only allows synchronous expressions.
749+
750+
## Using `unwrap` to replace `require(...).toNot(beNil())`
751+
752+
It's very common to require that a value not be nil. Instead of writing
753+
`try require(...).toNot(beNil())`, Nimble provides the `unwrap` function. This
754+
expression throws an error if the expression evaluates to nil, or returns the
755+
non-nil result when it passes. For example:
756+
757+
```swift
758+
let value = try unwrap(nil as Int?) // throws
759+
let value = try unwrap(1 as Int?) // returns 1
760+
```
761+
762+
Additionally, there is also the `pollUnwrap` function, which aliases to
763+
`require(...).toEventuallyNot(beNil())`. This is extremely useful for verifying
764+
that a value that is updated on a background thread was eventually set to a
765+
non-nil value.
766+
767+
Note: As with `require`, there are `unwraps`, `unwrapa`, `pollUnwraps`, and
768+
`pollUnwrapa` variants for allowing you to use autoclosures specifically with
769+
synchronous or asynchronous code.
770+
771+
## Throwing a Custom Error from Require
772+
773+
By default, if the matcher fails in a `require`, then a `RequireError` will be
774+
thrown. You can override this behavior and throw a custom error by passing a
775+
non-nil `Error` value to the `customError` parameter:
776+
777+
```swift
778+
try require(1).to(equal(2)) // throws a `RequireError`
779+
try require(customError: MyCustomError(), 1).to(equal(2)) // throws a `MyCustomError`
780+
```
680781

681782
# Built-in Matcher Functions
682783

0 commit comments

Comments
 (0)