Created
February 15, 2021 00:54
-
-
Save younata/18bd56aac306a9e0a84954108bfd6f90 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
class FakeCall<T> { | |
private(set) var calls = [T]() | |
func record(_ arguments: T) { | |
calls.append(arguments) | |
} | |
func reset() { | |
calls = [] | |
} | |
func lastCall<U>(_ keyPath: KeyPath<T, U>) -> U? { | |
guard let lastCall = calls.last else { return nil } | |
return lastCall[keyPath: keyPath] | |
} | |
} | |
extension FakeCall where T == Void { | |
func record() { | |
record(()) | |
} | |
} | |
class StubbedFakeCall<T, U>: FakeCall<T> { | |
private let factory: () -> U | |
private(set) var instances = [U]() | |
var lastInstance: U? { instances.last } | |
init(_ factory: @escaping () -> U) { | |
self.factory = factory | |
super.init() | |
} | |
override func reset() { | |
super.reset() | |
instances = [] | |
} | |
func stub(_ arguments: T) -> U { | |
self.record(arguments) | |
let instance = factory() | |
instances.append(instance) | |
return instance | |
} | |
} | |
extension StubbedFakeCall where T == Void { | |
func stub() -> U { | |
return stub(()) | |
} | |
} | |
class StubbedCombineFakeCall<T, U, E: Error>: FakeCall<T> { | |
private(set) var instances = [PassthroughSubject<U, E>]() | |
var lastInstance: PassthroughSubject<U, E>? { instances.last } | |
override func reset() { | |
super.reset() | |
instances = [] | |
} | |
func stub(_ arguments: T) -> AnyPublisher<U, E> { | |
self.record(arguments) | |
let instance = PassthroughSubject<U, E>() | |
instances.append(instance) | |
return instance.eraseToAnyPublisher() | |
} | |
} | |
extension StubbedCombineFakeCall where T == Void { | |
func stub() -> AnyPublisher<U, E> { | |
return stub(()) | |
} | |
} | |
import Quick | |
import Nimble | |
func beCalled<T>(times: Int) -> Predicate<FakeCall<T>> { | |
return Predicate { (received: Expression<FakeCall<T>>) in | |
let message = ExpectationMessage.expectedTo("be called \(times) time(s)") | |
guard let fakeCall: FakeCall<T> = try received.evaluate() else { | |
return PredicateResult(status: .fail, message: message.appendedBeNilHint()) | |
} | |
return PredicateResult(bool: fakeCall.calls.count == times, message: message) | |
} | |
} | |
func beCalled<T>(_ matchers: [Predicate<T>]) -> Predicate<FakeCall<T>> { | |
return Predicate { (received: Expression<FakeCall<T>>) in | |
let message = ExpectationMessage.expectedTo("have calls") | |
guard let fakeCall: FakeCall<T> = try received.evaluate() else { | |
return PredicateResult(status: .fail, message: message.appendedBeNilHint()) | |
} | |
guard fakeCall.calls.count == matchers.count else { | |
return PredicateResult(status: .fail, message: message.appended(message: "But number of calls did not match")) | |
} | |
var postfixMessages = [String]() | |
var matches = true | |
for (call, matcher) in zip(fakeCall.calls, matchers) { | |
let expression = Expression<T>(expression: { call }, location: received.location) | |
let result = try matcher.satisfies(expression) | |
if result.toBoolean(expectation: .toNotMatch) { | |
matches = false | |
} | |
postfixMessages.append("{\(result.message.expectedMessage)}") | |
} | |
var msg: ExpectationMessage | |
if let actualValue = try received.evaluate() { | |
msg = .expectedCustomValueTo( | |
"have calls: " + postfixMessages.joined(separator: ", and "), | |
"\(actualValue)" | |
) | |
} else { | |
msg = .expectedActualValueTo( | |
"have calls: " + postfixMessages.joined(separator: ", and ") | |
) | |
} | |
return PredicateResult(bool: matches, message: msg) | |
} | |
} | |
func beCalled<T>(_ expectations: [FakeCallPredicate<T>]) -> Predicate<FakeCall<T>> { | |
let predicates = expectations.map { (expectation: FakeCallPredicate<T>) -> Predicate<FakeCall<T>> in expectation.predicate } | |
return satisfyAllOf(predicates) | |
} | |
func satisfyAllOf<T>(_ predicates: [Predicate<T>]) -> Predicate<T> { | |
return Predicate.define { actualExpression in | |
var postfixMessages = [String]() | |
var status: PredicateStatus = .matches | |
for predicate in predicates { | |
let result = try predicate.satisfies(actualExpression) | |
if result.status == .fail { | |
status = .fail | |
} else if result.toBoolean(expectation: .toNotMatch), status != .fail { | |
status = .doesNotMatch | |
} | |
postfixMessages.append("{\(result.message.expectedMessage)}") | |
} | |
var msg: ExpectationMessage | |
if let actualValue = try actualExpression.evaluate() { | |
msg = .expectedCustomValueTo( | |
"match all of: " + postfixMessages.joined(separator: ", and "), | |
"\(actualValue)" | |
) | |
} else { | |
msg = .expectedActualValueTo( | |
"match all of: " + postfixMessages.joined(separator: ", and ") | |
) | |
} | |
return PredicateResult(status: status, message: msg) | |
} | |
} | |
// This compiles *significantly* faster than doing the same thing as a closure. No idea why. | |
struct FakeCallPredicate<T> { | |
let predicate: Predicate<FakeCall<T>> | |
private init(_ predicate: Predicate<FakeCall<T>>) { | |
self.predicate = predicate | |
} | |
static func expect<U>(_ keyPath: KeyPath<T, U>, to matcher: Predicate<U>) -> FakeCallPredicate<T> { | |
return FakeCallPredicate(Predicate { (received: Expression<FakeCall<T>>) -> PredicateResult in | |
guard let fakeCall = try received.evaluate(), let value: U = fakeCall.calls.last?[keyPath: keyPath] else { | |
return PredicateResult(status: .fail, message: ExpectationMessage.expectedTo("Be called").appendedBeNilHint()) | |
} | |
let expression: Expression<U> = Expression(expression: { value }, location: received.location) | |
let matcherResult = try matcher.satisfies(expression) | |
let message = matcherResult.message.wrappedExpectation(before: "Have been called with \(keyPath)", after: "On the most recent call") | |
return PredicateResult(status: matcherResult.status, message: message) | |
}) | |
} | |
static func expect<U>(_ closure: @escaping (T) -> U?, to matcher: Predicate<U>) -> FakeCallPredicate<T> { | |
return FakeCallPredicate(Predicate { (received: Expression<FakeCall<T>>) -> PredicateResult in | |
guard let fakeCall = try received.evaluate(), let lastCall: T = fakeCall.calls.last else { | |
return PredicateResult(status: .fail, message: ExpectationMessage.expectedTo("Be called").appendedBeNilHint()) | |
} | |
let expression: Expression<U> = Expression(expression: { closure(lastCall) }, location: received.location) | |
let matcherResult = try matcher.satisfies(expression) | |
let message = matcherResult.message.wrappedExpectation(before: "Have been called (closure)", after: "On the most recent call") | |
return PredicateResult(status: matcherResult.status, message: message) | |
}) | |
} | |
} | |
enum AssertionResult { | |
case succeeded | |
case failed | |
var boolValue: Bool { | |
switch self { | |
case .succeeded: return true | |
case .failed: return false | |
} | |
} | |
} | |
func haveResults(_ results: [AssertionResult]) -> Predicate<[AssertionRecord]> { | |
return Predicate { (received: Expression<[AssertionRecord]>) in | |
let message = ExpectationMessage.expectedActualValueTo("have results \(results)") | |
guard let records = try received.evaluate() else { | |
return PredicateResult(status: .fail, message: message.appendedBeNilHint()) | |
} | |
guard results.count == records.count else { | |
return PredicateResult(status: .fail, message: message.appended(details: "Number of results do not match")) | |
} | |
let allPassed = records.enumerated().allSatisfy { (idx, record) in | |
return record.success == results[idx].boolValue | |
} | |
return PredicateResult(bool: allPassed, message: message) | |
} | |
} | |
final class FakeCallSpec: QuickSpec { | |
override func spec() { | |
var subject = FakeCall<(Int, String)>() | |
beforeEach { | |
subject = FakeCall<(Int, String)>() | |
} | |
describe("beCalled(times:)") { | |
beforeEach { | |
subject.record((1, "")) | |
} | |
it("matches when the receiver has been called the specified amount of times") { | |
let records: [AssertionRecord] = gatherExpectations(silently: true) { | |
expect(subject).toNot(beCalled(times: 1)) | |
expect(subject).to(beCalled(times: 1)) | |
expect(subject).toNot(beCalled(times: 0)) | |
expect(subject).to(beCalled(times: 0)) | |
} | |
expect(records).to(haveResults([ | |
.failed, | |
.succeeded, | |
.succeeded, | |
.failed | |
])) | |
} | |
} | |
describe("beCalled(_ with:)") { | |
beforeEach { | |
subject.record((1, "")) | |
subject.record((2, "hello")) | |
} | |
it("matches when the last call to the receiver has been called with the specific arguments") { | |
let records: [AssertionRecord] = gatherExpectations(silently: true) { | |
expect(subject).to(beCalled([ | |
.expect(\.0, to: equal(2)), | |
.expect(\.1, to: equal("hello")) | |
])) | |
expect(subject).toNot(beCalled([ | |
.expect(\.0, to: equal(2)), | |
.expect(\.1, to: equal("hello")) | |
])) | |
expect(subject).to(beCalled([ | |
.expect(\.0, to: equal(1)), | |
.expect(\.1, to: equal("")) | |
])) | |
expect(subject).toNot(beCalled([ | |
.expect(\.0, to: equal(1)), | |
.expect(\.1, to: equal("")) | |
])) | |
} | |
expect(records).to(haveResults([ | |
.succeeded, | |
.failed, | |
.failed, | |
.succeeded | |
])) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment