Skip to content

Instantly share code, notes, and snippets.

@timshadel
Created February 16, 2019 20:23
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 timshadel/64eded00cc9976f0c335a79f2955418c to your computer and use it in GitHub Desktop.
Save timshadel/64eded00cc9976f0c335a79f2955418c to your computer and use it in GitHub Desktop.
XCTest custom matchers
//
// 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