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

Add a map matcher. #1112

Merged
merged 1 commit into from
Jan 6, 2024
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
10 changes: 9 additions & 1 deletion Nimble.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@
8922828A2B2833B7002DA355 /* PollingTest+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892282892B2833B7002DA355 /* PollingTest+Require.swift */; };
8922828D2B283818002DA355 /* Polling+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8922828B2B2837E1002DA355 /* Polling+Require.swift */; };
8922828F2B283956002DA355 /* AsyncAwaitTest+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8922828E2B283956002DA355 /* AsyncAwaitTest+Require.swift */; };
8923E60D2B47CE7E00F3961A /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8923E60C2B47CE7E00F3961A /* Map.swift */; };
8923E6102B47D08300F3961A /* MapTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8923E60E2B47D06E00F3961A /* MapTest.swift */; };
892FDF1329D3EA7700523A80 /* AsyncExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892FDF1229D3EA7700523A80 /* AsyncExpression.swift */; };
896962412A5FABD000A7929D /* AsyncAllPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962402A5FABD000A7929D /* AsyncAllPass.swift */; };
8969624A2A5FAD5F00A7929D /* AsyncAllPassTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */; };
Expand Down Expand Up @@ -314,6 +316,8 @@
892282892B2833B7002DA355 /* PollingTest+Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollingTest+Require.swift"; sourceTree = "<group>"; };
8922828B2B2837E1002DA355 /* Polling+Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Polling+Require.swift"; sourceTree = "<group>"; };
8922828E2B283956002DA355 /* AsyncAwaitTest+Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncAwaitTest+Require.swift"; sourceTree = "<group>"; };
8923E60C2B47CE7E00F3961A /* Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Map.swift; sourceTree = "<group>"; };
8923E60E2B47D06E00F3961A /* MapTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTest.swift; sourceTree = "<group>"; };
892FDF1229D3EA7700523A80 /* AsyncExpression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncExpression.swift; sourceTree = "<group>"; };
896962402A5FABD000A7929D /* AsyncAllPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPass.swift; sourceTree = "<group>"; };
896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPassTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -524,6 +528,7 @@
1F925EFE195C187600ED456B /* EndWithTest.swift */,
1F925F04195C18B700ED456B /* EqualTest.swift */,
472FD1361B9E094B00C7B8DA /* HaveCountTest.swift */,
8923E60E2B47D06E00F3961A /* MapTest.swift */,
AE7ADE481C80C00D00B94CD3 /* MatchErrorTest.swift */,
DDB4D5EF19FE442800E9D9FE /* MatchTest.swift */,
1FCF914E1C61C85A00B15DCB /* PostNotificationTest.swift */,
Expand Down Expand Up @@ -583,10 +588,11 @@
C576224C2A61D3AE00BD6A8C /* Equal+TupleArray.swift */,
472FD1341B9E085700C7B8DA /* HaveCount.swift */,
DDB4D5EC19FE43C200E9D9FE /* Match.swift */,
8923E60C2B47CE7E00F3961A /* Map.swift */,
1FD8CD1D1968AB07008ED995 /* MatcherProtocols.swift */,
AE7ADE441C80BF8000B94CD3 /* MatchError.swift */,
1FCF91521C61C8A400B15DCB /* PostNotification.swift */,
1FA0C3FE1E30B14500623165 /* Matcher.swift */,
1FCF91521C61C8A400B15DCB /* PostNotification.swift */,
1FD8CD1E1968AB07008ED995 /* RaisesException.swift */,
A8F6B5BC2070186D00FCB5ED /* SatisfyAllOf.swift */,
7B5358BD1C38479700A23FAA /* SatisfyAnyOf.swift */,
Expand Down Expand Up @@ -850,6 +856,7 @@
29EA59671B551EE6002D767E /* ThrowError.swift in Sources */,
62FB326223B78BF90047BED9 /* BeginWithPrefix.swift in Sources */,
1FD8CD5B1968AB07008ED995 /* Equal.swift in Sources */,
8923E60D2B47CE7E00F3961A /* Map.swift in Sources */,
CDF5C57B2647B89B0036532C /* Equal+Tuple.swift in Sources */,
857D1849253610A900D8693A /* BeWithin.swift in Sources */,
1FD8CD4D1968AB07008ED995 /* BeLessThan.swift in Sources */,
Expand Down Expand Up @@ -975,6 +982,7 @@
DD72EC651A93874A002F7651 /* AllPassTest.swift in Sources */,
1F4A569E1A3B3565009E1637 /* ObjCMatchTest.m in Sources */,
1F925EEA195C124400ED456B /* BeAnInstanceOfTest.swift in Sources */,
8923E6102B47D08300F3961A /* MapTest.swift in Sources */,
29EA59641B551ED2002D767E /* ThrowErrorTest.swift in Sources */,
6CAEDD0B1CAEA86F003F1584 /* LinuxSupport.swift in Sources */,
1F4A566B1A3B3108009E1637 /* ObjCBeAnInstanceOfTest.m in Sources */,
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,38 @@ The `String` provided with `.failed()` is shown when the test fails.

When using `toEventually()` be careful not to make state changes or run process intensive code since this closure will be ran many times.

## Mapping a Value to Another Value

Sometimes, you only want to match against a property or group of properties.
For example, if you wanted to check that only one or a few properties of a value
are equal to something else. For this, use the `map` matcher to convert a value
to another value and check it with a matcher.

```swift
// Swift

expect(someValue).to(map(\.someProperty, equal(expectedProperty)))

// or, for checking multiple different properties:

expect(someValue).to(satisfyAllOf(
map(\.firstProperty, equal(expectedFirstProperty)),
map({ $0.secondProperty }, equal(expectedSecondProperty))
))
```

The `map` matcher takes in either a closure or a keypath literal, and a matcher
to compose with. It also works with async closures and async matchers.

In most cases, it is simpler and easier to not use map (that is, prefer
`expect(someValue.property).to(equal(1))` to
`expect(someValue).to(map(\.property, equal(1)))`). But `map` is incredibly
useful when combined with `satisfyAllOf`/`satisfyAnyOf`, especially for checking
a value that cannot conform to `Equatable` (or you don't want to make it
conform to `Equatable`). However, if you find yourself reusing `map` many times
to do a fuzzy-equals of a given type, you will find writing a custom matcher to
be much easier to use and maintain.

# Writing Your Own Matchers

In Nimble, matchers are Swift functions that take an expected
Expand Down
16 changes: 12 additions & 4 deletions Sources/Nimble/AsyncExpression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,27 @@ public struct AsyncExpression<Value> {
/// - Parameter block: The block that can cast the current Expression value to a
/// new type.
public func cast<U>(_ block: @escaping (Value?) throws -> U?) -> AsyncExpression<U> {
return AsyncExpression<U>(
AsyncExpression<U>(
expression: ({ try await block(self.evaluate()) }),
location: self.location,
isClosure: self.isClosure
)
}

public func cast<U>(_ block: @escaping (Value?) async throws -> U?) -> AsyncExpression<U> {
AsyncExpression<U>(
expression: ({ try await block(self.evaluate()) }),
location: self.location,
isClosure: self.isClosure
)
}

public func evaluate() async throws -> Value? {
return try await self._expression(_withoutCaching)
try await self._expression(_withoutCaching)
}

public func withoutCaching() -> AsyncExpression<Value> {
return AsyncExpression(
AsyncExpression(
memoizedExpression: self._expression,
location: location,
withoutCaching: true,
Expand All @@ -112,7 +120,7 @@ public struct AsyncExpression<Value> {
}

public func withCaching() -> AsyncExpression<Value> {
return AsyncExpression(
AsyncExpression(
memoizedExpression: memoizedClosure { try await self.evaluate() },
location: self.location,
withoutCaching: false,
Expand Down
27 changes: 27 additions & 0 deletions Sources/Nimble/Matchers/Map.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// `map` works by transforming the expression to a value that the given matcher uses.
///
/// For example, you might only care that a particular property on a method equals some other value.
/// So, you could write `expect(myObject).to(lens(\.someIntValue, equal(3))`.
/// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object.
public func map<T, U>(_ transform: @escaping (T) throws -> U, _ matcher: Matcher<U>) -> Matcher<T> {
Matcher { (received: Expression<T>) in
try matcher.satisfies(received.cast { value in
guard let value else { return nil }
return try transform(value)
})
}
}

/// `map` works by transforming the expression to a value that the given matcher uses.
///
/// For example, you might only care that a particular property on a method equals some other value.
/// So, you could write `expect(myObject).to(lens(\.someIntValue, equal(3))`.
/// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object.
public func map<T, U>(_ transform: @escaping (T) async throws -> U, _ matcher: some AsyncableMatcher<U>) -> AsyncMatcher<T> {
AsyncMatcher { (received: AsyncExpression<T>) in
try await matcher.satisfies(received.cast { value in
guard let value else { return nil }
return try await transform(value)
})
}
}
83 changes: 83 additions & 0 deletions Tests/NimbleTests/Matchers/MapTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import XCTest
import Nimble
#if SWIFT_PACKAGE
import NimbleSharedTestHelpers
#endif

final class MapTest: XCTestCase {
func testMap() {
expect(1).to(map({ $0 }, equal(1)))

struct Value {
let int: Int
let string: String?
}

expect(Value(
int: 1,
string: "hello"
)).to(satisfyAllOf(
map(\.int, equal(1)),
map(\.string, equal("hello"))
))

expect(Value(
int: 1,
string: "hello"
)).to(satisfyAnyOf(
map(\.int, equal(2)),
map(\.string, equal("hello"))
))

expect(Value(
int: 1,
string: "hello"
)).toNot(satisfyAllOf(
map(\.int, equal(2)),
map(\.string, equal("hello"))
))
}

func testMapAsync() async {
struct Value {
let int: Int
let string: String
}

await expect(Value(
int: 1,
string: "hello"
)).to(map(\.int, asyncEqual(1)))

await expect(Value(
int: 1,
string: "hello"
)).toNot(map(\.int, asyncEqual(2)))
}

func testMapWithAsyncFunction() async {
func someOperation(_ value: Int) async -> String {
"\(value)"
}
await expect(1).to(map(someOperation, equal("1")))
}

func testMapWithActor() {
actor Box {
let int: Int
let string: String

init(int: Int, string: String) {
self.int = int
self.string = string
}
}

let box = Box(int: 3, string: "world")

expect(box).to(satisfyAllOf(
map(\.int, equal(3)),
map(\.string, equal("world"))
))
}
}
Loading