Skip to content

Instantly share code, notes, and snippets.

@younata
Created February 15, 2021 00:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save younata/18bd56aac306a9e0a84954108bfd6f90 to your computer and use it in GitHub Desktop.
Save younata/18bd56aac306a9e0a84954108bfd6f90 to your computer and use it in GitHub Desktop.
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