Created
February 16, 2019 20:23
-
-
Save timshadel/64eded00cc9976f0c335a79f2955418c to your computer and use it in GitHub Desktop.
XCTest custom matchers
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
// | |
// XCTestCase+Matchers.swift | |
// greatwork | |
// | |
// Created by Tim on 4/14/17. | |
// Copyright © 2017 OC Tanner Company, Inc. All rights reserved. | |
// | |
import Foundation | |
import XCTest | |
import Marshal | |
import Reactor | |
private enum Keys { | |
static let id = "id" | |
} | |
extension XCTestCase { | |
// MARK: - Throws | |
func shouldNotThrow(file: String = #file, line: UInt = #line, _ block: () throws -> Void) { | |
do { | |
_ = try block() | |
} catch { | |
recordFailure(withDescription: "Boo! \(error)", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func shouldThrow(file: String = #file, line: UInt = #line, _ block: () throws -> Void) { | |
do { | |
_ = try block() | |
recordFailure(withDescription: "Should have thrown!", inFile: file, atLine: Int(line), expected: true) | |
} catch { | |
} | |
} | |
// MARK: - Equals | |
func expect(nil expression: @autoclosure () -> Any?, file: String = #file, line: UInt = #line) { | |
if let it = expression() { | |
recordFailure(withDescription: "Expected '\(it)' to be nil.", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect(notNil expression: @autoclosure () -> Any?, file: String = #file, line: UInt = #line) { | |
if expression() == nil { | |
recordFailure(withDescription: "Expected this not to be nil.", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect(exists element: XCUIElement, file: String = #file, line: UInt = #line) { | |
if !element.exists { | |
recordFailure(withDescription: "Expected \(element) to exist.", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect(doesNotExist element: XCUIElement, file: String = #file, line: UInt = #line) { | |
if element.exists { | |
recordFailure(withDescription: "Expected \(element) to not exist.", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect(false expression: @autoclosure () -> Bool?, file: String = #file, line: UInt = #line) { | |
guard let actual = expression() else { | |
recordFailure(withDescription: "Expected 'nil' to be false.", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
if actual != false { | |
recordFailure(withDescription: "Expected this to be false.", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect(true expression: @autoclosure () -> Bool?, file: String = #file, line: UInt = #line) { | |
guard let actual = expression() else { | |
recordFailure(withDescription: "Expected 'nil' to be true.", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
if actual != true { | |
recordFailure(withDescription: "Expected this to be true.", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect<T: Equatable>(_ this: @autoclosure () -> T?, equals expression: @autoclosure () -> T?, file: String = #file, line: UInt = #line) { | |
let actual = this() | |
let expected = expression() | |
if !equals(actual, expected) { | |
recordFailure(withDescription: "Expected '\(String(describing: actual))' to equal '\(String(describing: expected))]", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect<T: Equatable>(_ this: @autoclosure () -> T?, notEquals expression: @autoclosure () -> T?, file: String = #file, line: UInt = #line) { | |
let actual = this() | |
let expected = expression() | |
if equals(actual, expected) { | |
recordFailure(withDescription: "Expected '\(String(describing: actual))' to not equal '\(String(describing: expected))]", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect(date thisDate: Date?, equals thatDate: Date?, downToThe component: Calendar.Component = .second, file: String = #file, line: UInt = #line) { | |
guard let thisDate = thisDate, let thatDate = thatDate else { | |
recordFailure(withDescription: "Expected dates to not be nil", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
if !Calendar.current.isDate(thisDate, equalTo: thatDate, toGranularity: component) { | |
recordFailure(withDescription: "Expected \(thisDate) to equal: \(thatDate)", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
// MARK: - Contains | |
func expect(_ actual: String, contains expected: String..., file: String = #file, line: UInt = #line) { | |
let result = expected.map { actual.contains($0) } | |
if let index = result.index(of: false) { | |
recordFailure(withDescription: "Expected \(actual) to contain \(expected[index])", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect(_ actual: String, doesNotContain expected: String..., file: String = #file, line: UInt = #line) { | |
let result = expected.map { actual.contains($0) } | |
if let index = result.index(of: true) { | |
recordFailure(withDescription: "Expected \(actual) to not contain \(expected[index])", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
// MARK: - Async | |
typealias AsyncExecution = () -> Void | |
fileprivate static var defaultTimeout: TimeInterval { return 5.0 } | |
func async(description: String = "Waiting", _ block: (AsyncExecution) -> Void) { | |
let waiter = expectation(description: description) | |
block { | |
waiter.fulfill() | |
} | |
wait(for: [waiter], timeout: 5) | |
} | |
func expectEventually(nil expression: @autoclosure @escaping () -> Any?, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
let expected = expectation { () -> Bool in | |
return expression() == nil | |
} | |
wait(for: "this", toEventually: "be nil", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
func expectEventually(notNil expression: @autoclosure @escaping () -> Any?, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
let expected = expectation { () -> Bool in | |
return expression() != nil | |
} | |
wait(for: "this", toEventually: "not be nil", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
func expectEventually(exists element: XCUIElement, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
let expected = expectation { () -> Bool in | |
return element.exists | |
} | |
wait(for: element.description, toEventually: "exist", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
func expectEventually(doesNotExist element: XCUIElement, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
let expected = expectation { () -> Bool in | |
return !element.exists | |
} | |
wait(for: element.description, toEventually: "not exist", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
func expectEventually(false expression: @autoclosure @escaping () -> Bool, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
let expected = expectation { () -> Bool in | |
return expression() == false | |
} | |
wait(for: "this", toEventually: "be false", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
func expectEventually(true expression: @autoclosure @escaping () -> Bool, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
let expected = expectation { () -> Bool in | |
return expression() == true | |
} | |
wait(for: "this", toEventually: "be true", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
func expectEventually<T: Equatable>(_ this: @autoclosure @escaping () -> T, equals expression: @autoclosure @escaping () -> T, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
var lastActual = this() | |
var lastExpected = expression() | |
guard lastActual != lastExpected else { return } | |
let expected = expectation { () -> Bool in | |
lastActual = this() | |
lastExpected = expression() | |
return lastActual == lastExpected | |
} | |
wait(for: "'\(lastActual)'", toEventually: "equal '\(lastExpected)'", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
func expectEventually<T: Equatable>(_ this: @autoclosure @escaping () -> T?, equals expression: @autoclosure @escaping () -> T, timeout: TimeInterval = XCTestCase.defaultTimeout, file: String = #file, line: UInt = #line) { | |
var lastActual = this() | |
var lastExpected = expression() | |
guard let actual = lastActual, actual == lastExpected else { return } | |
let expected = expectation { () -> Bool in | |
lastActual = this() | |
lastExpected = expression() | |
guard let actual = lastActual else { return false } | |
return actual == lastExpected | |
} | |
let actualString = (lastActual == nil) ? "nil" : "\(lastActual!)" | |
wait(for: "'\(actualString)'", toEventually: "equal '\(lastExpected)'", timeout: timeout, with: [expected], file: file, line: line) | |
} | |
private func equals<T: Equatable>(_ lhs: T?, _ rhs: T?) -> Bool { | |
if lhs == nil && rhs == nil { | |
return true | |
} | |
guard let actual = lhs, let expected = rhs else { | |
return false | |
} | |
return actual == expected | |
} | |
private func expectation(from block: @escaping () -> Bool) -> XCTestExpectation { | |
let predicate = NSPredicate { _, _ -> Bool in | |
return block() | |
} | |
let expected = expectation(for: predicate, evaluatedWith: NSObject()) | |
return expected | |
} | |
private func wait(for subject: String, toEventually outcome: String, timeout: TimeInterval, with expectations: [XCTestExpectation], file: String, line: UInt) { | |
let result = XCTWaiter().wait(for: expectations, timeout: timeout) | |
switch result { | |
case .completed: | |
return | |
case .timedOut: | |
self.recordFailure(withDescription: "Expected \(subject) to eventually \(outcome). Timed out after \(timeout)s.", inFile: file, atLine: Int(line), expected: true) | |
default: | |
self.recordFailure(withDescription: "Unexpected result while waiting for \(subject) to eventually \(outcome): \(result)", inFile: file, atLine: Int(line), expected: false) | |
} | |
} | |
} | |
// MARK: - Marshal | |
extension XCTestCase { | |
private func expect(marshaled this: MarshaledObject, equals expression: MarshaledObject, file: String, line: UInt) { | |
let actual = this as! NSDictionary | |
let expected = expression as! NSDictionary | |
if actual != expected { | |
recordFailure(withDescription: "Expected '\(actual)' to equal '\(expected)'", inFile: file, atLine: Int(line), expected: true) | |
} | |
} | |
func expect<T: Marshaling>(marshaled this: @autoclosure () -> T, equals expression: @autoclosure () -> MarshaledObject, file: String = #file, line: UInt = #line) { | |
expect(marshaled: this().marshaled(), equals: expression(), file: file, line: line) | |
} | |
func expect<T: Marshaling>(marshaled this: @autoclosure () -> T?, equals expression: @autoclosure () -> MarshaledObject, file: String = #file, line: UInt = #line) { | |
guard let actual = this() else { | |
recordFailure(withDescription: "Expected 'nil' to equal '\(expression())'", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
expect(marshaled: actual, equals: expression(), file: file, line: line) | |
} | |
func expect(_ lhs: JSONObject?, equalsJSONObject rhs: JSONObject?, file: String = #file, line: UInt = #line) { | |
if lhs == nil && rhs == nil { | |
return // Success | |
} | |
guard var lhs = lhs, var rhs = rhs else { | |
recordFailure(withDescription: "Expected objects to not be nil.", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
lhs.removeValue(forKey: "ref") | |
rhs.removeValue(forKey: "ref") | |
lhs.removeValue(forKey: "calendars") | |
rhs.removeValue(forKey: "calendars") | |
guard lhs.count == rhs.count else { | |
let lhsKeys = Set(lhs.map { $0.key }) | |
let rhsKeys = Set(rhs.map { $0.key }) | |
recordFailure(withDescription: "Objects do not have the same number of child values. Expected \(lhs.count) for \(String(describing: lhs[Keys.id])), got \(rhs.count) for \(String(describing: rhs[Keys.id])). Missing keys: \(lhsKeys.subtracting(rhsKeys)), extra keys: \(rhsKeys.subtracting(lhsKeys))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
for (key, value) in lhs { | |
if let object = value as? JSONObject { | |
guard let rhsObject = rhs[key] as? JSONObject else { | |
recordFailure(withDescription: "Missing object for \(key) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
expect(object, equalsJSONObject: rhsObject, file: file, line: line) | |
} else if let object = value as? [JSONObject] { | |
guard let rhsObject = rhs[key] as? [JSONObject] else { | |
recordFailure(withDescription: "Missing array of objects for \(key) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
for (index, subObject) in object.enumerated() { | |
expect(subObject, equalsJSONObject: rhsObject[index], file: file, line: line) | |
} | |
} else { | |
guard let rhsValue = rhs[key] else { | |
recordFailure(withDescription: "Missing value for \(key) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
if type(of: value) != type(of: rhsValue) { | |
if let _ = value as? Set<String>, let _ = rhsValue as? [String] { | |
// continue | |
} else { | |
recordFailure(withDescription: "Type of values does not match for \(key). Expected \(type(of: value)) in \(String(describing: lhs[Keys.id])), got \(type(of: rhsValue)) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
} | |
if let intValue = value as? Int, let intRhsValue = rhsValue as? Int { | |
guard intValue == intRhsValue else { | |
recordFailure(withDescription: "Integer values do not match for \(key). Expected \(intValue) in \(String(describing: lhs[Keys.id])), got \(intRhsValue) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
} else if let doubleValue = value as? Double, let doubleRhsValue = rhsValue as? Double { | |
guard doubleValue == doubleRhsValue else { | |
recordFailure(withDescription: "Double values do not match for \(key). Expected \(doubleValue) in \(String(describing: lhs[Keys.id])), got \(doubleRhsValue) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
} else if let stringValue = value as? String, let stringRhsValue = rhsValue as? String { | |
guard stringValue == stringRhsValue else { | |
recordFailure(withDescription: "String values do not match for \(key). Expected \(stringValue) in \(String(describing: String(describing: lhs[Keys.id]))), got \(stringRhsValue) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
} else if let boolValue = value as? Bool, let boolRhsValue = rhsValue as? Bool { | |
guard boolValue == boolRhsValue else { | |
recordFailure(withDescription: "Bool values do not match for \(key). Expected \(boolValue) in \(String(describing: String(describing: lhs[Keys.id]))), got \(boolRhsValue) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
} else if let stringSet = value as? Set<String>, let rhsStringArray = rhsValue as? [String] { | |
guard stringSet == Set(rhsStringArray) else { | |
recordFailure(withDescription: "String set values do not match for \(key). Expected \(stringSet) in \(String(describing: lhs[Keys.id])), got \(Set(rhsStringArray)) in \(String(describing: rhs[Keys.id]))", inFile: file, atLine: Int(line), expected: true) | |
return | |
} | |
} | |
} | |
} | |
} | |
} | |
// MARK: - Reactor | |
fileprivate struct IdleCommand<StateType: State>: Command { | |
let expectation: XCTestExpectation | |
func execute(state: StateType, core: Core<StateType>) { | |
expectation.fulfill() | |
} | |
} | |
extension XCTestCase { | |
func waitForIdle<StateType>(in core: Core<StateType>) { | |
let expect = expectation(description: "Waiting for core to process.") | |
core.fire(command: IdleCommand(expectation: expect)) | |
wait(for: [expect], timeout: 5) | |
} | |
func wait<StateType, CommandType: Command>(for command: CommandType, in core: Core<StateType>) where CommandType.StateType == StateType { | |
core.fire(command : command) | |
let expect = expectation(description: "Waiting for core to process.") | |
core.fire(command: IdleCommand(expectation: expect)) | |
wait(for: [expect], timeout: 5) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment