MobiusTest/Source/FirstMatchers.swift (114 lines of code) (raw):

// Copyright Spotify AB. // SPDX-License-Identifier: Apache-2.0 import Foundation import MobiusCore import XCTest public typealias FirstPredicate<Model, Effect> = Predicate<First<Model, Effect>> /// Function to produce an `AssertFirst` function to be used with the `InitSpec` /// /// - Parameter predicates: Nimble `Predicate` that verifies a first. Can be produced through `FirstMatchers` /// - Returns: An `AssertFirst` function to be used with the `InitSpec` public func assertThatFirst<Model, Effect>( _ predicates: FirstPredicate<Model, Effect>..., failFunction: @escaping AssertionFailure = XCTFail ) -> AssertFirst<Model, Effect> { return { (result: First<Model, Effect>) in predicates.forEach({ predicate in let predicateResult = predicate(result) if case .failure(let message, let file, let line) = predicateResult { failFunction(message, file, line) } }) } } /// Returns a `Predicate` that matches `First` instances with a M that is equal to the supplied one. /// /// - Parameter expected: the expected M /// - Returns: a `Predicate` determening if a `First` contains the expected M public func hasModel<Model: Equatable, Effect>( _ expected: Model, file: StaticString = #file, line: UInt = #line ) -> FirstPredicate<Model, Effect> { return { (first: First<Model, Effect>) in if first.model != expected { return .failure( message: "Different model than expected (−), got (+): \n" + "\(dumpDiff(expected, first.model))", file: file, line: line ) } return .success } } /// Returns a `Predicate` that matches `First` instances with no Es. /// /// - Returns: a `Predicate` determening if a `First` contains no Es public func hasNoEffects<Model, Effect>( file: StaticString = #file, line: UInt = #line ) -> FirstPredicate<Model, Effect> { return { (first: First<Model, Effect>) in if !first.effects.isEmpty { return .failure( message: "Expected no effects, got <\(first.effects)>", file: file, line: line ) } return .success } } /// Returns a `Predicate` that matches if all the supplied Es are present in the supplied `First` in any order. /// The `First` may have more Es than the ones included. /// /// - Parameter Es: the Es to match (possibly empty) /// - Returns: a `Predicate` that matches `First` instances that include all the supplied Es public func hasEffects<Model, Effect: Equatable>( _ expected: [Effect], file: StaticString = #file, line: UInt = #line ) -> FirstPredicate<Model, Effect> { return { (first: First<Model, Effect>) in let actual = first.effects let unmatchedExpected = expected.filter { !actual.contains($0) } guard !unmatchedExpected.isEmpty else { return .success } // Find the effects that were produced but not expected - this is permitted, but there might be a close match // there let unmatchedActual = actual.filter { !expected.contains($0) } return .failure( message: "Missing \(countedEffects(unmatchedExpected, label: "expected")) (−), got (+)" + " (with \(countedEffects(unmatchedActual, label: "actual")) unmatched):\n" + dumpDiffFuzzy(expected: unmatchedExpected, actual: unmatchedActual, withUnmatchedActual: false), file: file, line: line ) } } /// Constructs a matcher that matches if only the supplied effects are present in the supplied `First`, in any order. /// /// - Parameter expected: the effects to match (possibly empty) /// - Returns: a `Predicate` that matches `First` instances that include all the supplied effects public func hasOnlyEffects<Model, Effect: Equatable>( _ expected: [Effect], file: StaticString = #file, line: UInt = #line ) -> FirstPredicate<Model, Effect> { return { (first: First<Model, Effect>) in let actual = first.effects let unmatchedExpected = expected.filter { !actual.contains($0) } let unmatchedActual = actual.filter { !expected.contains($0) } var errorString = [ !unmatchedExpected.isEmpty ? "missing \(countedEffects(unmatchedExpected, label: "expected")) (−)" : nil, !unmatchedActual.isEmpty ? "got \(countedEffects(unmatchedActual, label: "actual unmatched")) (+)" : nil, ].compactMap { $0 }.joined(separator: ", ") errorString = errorString.prefix(1).capitalized + errorString.dropFirst() if !errorString.isEmpty { return .failure( message: "\(errorString):\n" + dumpDiffFuzzy(expected: unmatchedExpected, actual: unmatchedActual, withUnmatchedActual: true), file: file, line: line ) } return .success } } /// Constructs a matcher that matches if the supplied effects are equal to the supplied `First`. /// /// - Parameter expected: the effects to match (possibly empty) /// - Returns: a `Predicate` that matches `First` instances that include all the supplied effects public func hasExactlyEffects<Model, Effect: Equatable>( _ expected: [Effect], file: StaticString = #file, line: UInt = #line ) -> FirstPredicate<Model, Effect> { return { (first: First<Model, Effect>) in if first.effects != expected { return .failure( message: "Different effects than expected (−), got (+): \n" + "\(dumpDiff(expected, first.effects))", file: file, line: line ) } return .success } } private func countedEffects<T>(_ effects: [T], label: String) -> String { let count = effects.count return count == 1 ? "1 \(label) effect" : "\(count) \(label) effects" }