Swift Hamcrest

Build Status](https://github.com/nschum/SwiftHamcrest/actions/workflows/build.yml/badge.svg) Swift 5.7 OS X ≥ 11.0 iOS ≥ 11.0 Carthage compatible](https://github.com/Carthage/Carthage) CocoaPods compatible

Hamcrest gives you advanced matchers with better error messages for your Swift unit tests。

Hamcrest 最初用 Java 编写,并可用于多种语言

教程

本教程也可在 Hamcrest 工作区中作为 Playground 使用。

通常,您在单元测试中使用这些匹配器,其中不匹配将导致测试失败,但它们也适用于 Playground,其中不匹配只会打印错误消息。

在任何一种情况下,都需要导入 Hamcrest 模块。

import Hamcrest

运算符匹配器

以下是非常简单的匹配器。匹配的表达式看起来像常规布尔表达式,但提供可读的不匹配消息,而不是通用错误。

let x = 1 + 1

// The comments show the human-readable error messages created by the assertions.

assertThat(x == 2) // ✓
assertThat(x == 3) // GOT: 2, EXPECTED: equal to 3

assertThat(x > 1) // ✓
assertThat(x > 2) // GOT: 2, EXPECTED: greater than 2

assertThat(x >= 2) // ✓
assertThat(x >= 3) // GOT: 2, EXPECTED: greater than or equal to 3

assertThat(x < 3) // ✓
assertThat(x < 2) // GOT: 2, EXPECTED: less than 2

assertThat(x <= 2) // ✓
assertThat(x <= 1) // GOT: 2, EXPECTED: less than or equal to 1

class Test {}
let o = Test()
assertThat(o === o) // ✓
assertThat(o === Test())
// GOT: __lldb_expr_8.Test (0x7f9572b020d0),
// EXPECTED: same instance as 0x7f9570c702a0

文本匹配器

所有这些匹配器也可用作函数。

assertThat(x, equalTo(2)) // ✓
assertThat(x, equalTo(3)) // GOT: 2, EXPECTED: equal to 3

assertThat(x, greaterThan(1)) // ✓
assertThat(x, greaterThan(2)) // GOT: 2, EXPECTED: greater than 2

assertThat(x, greaterThanOrEqualTo(2)) // ✓
assertThat(x, greaterThanOrEqualTo(3))
// GOT: 2, EXPECTED: greater than or equal to 3

assertThat(x, lessThan(3)) // ✓
assertThat(x, lessThan(2)) // GOT: 2, EXPECTED: less than 2

assertThat(x, lessThanOrEqualTo(2)) // ✓
assertThat(x, lessThanOrEqualTo(1))
// GOT: 2, EXPECTED: less than or equal to 1

assertThat(x, inInterval(1...2)) // ✓
assertThat(x, inInterval(1..<2)) // GOT: 2, EXPECTED: in interval 1..<2

assertThat(o, sameInstance(o)) // ✓
assertThat(o, sameInstance(Test()))
// GOT: __lldb_expr_53.Test, EXPECTED: same instance as __lldb_expr_53.Test

这里还有一些更直接的匹配器

assertThat("foobarbaz", containsString("bar")) // ✓
assertThat("foobarbaz", containsString("bla"))
// GOT: "foobarbaz", EXPECTED: contains "bla"

assertThat("foobarbaz", containsStringsInOrder("f", "b", "b")) // ✓
assertThat("foobarbaz", containsStringsInOrder("foo", "baz", "bar"))
// GOT: "foobarbaz", EXPECTED: contains in order ["foo", "baz", "bar"]

assertThat("foobarbaz", hasPrefix("foo")) // ✓
assertThat("foobarbaz", hasPrefix("oo"))
// GOT: "foobarbaz", EXPECTED: has prefix "oo"

assertThat("foobarbaz", hasSuffix("baz")) // ✓
assertThat("foobarbaz", hasSuffix("ba"))
// GOT: "foobarbaz", EXPECTED: has suffix "ba"

assertThat("ac", matchesPattern("\\b(a|b)(c|d)\\b")) // ✓
assertThat("BD", matchesPattern("\\b(a|b)(c|d)\\b", options: .caseInsensitive)) // ✓
assertThat("aC", matchesPattern("\\b(a|b)(c|d)\\b"))
// "GOT: "aC", EXPECTED: matches \b(a|b)(c|d)\b"

assertThat(10.0, closeTo(10.0, 0.01)) // ✓
assertThat(10.0000001, closeTo(10, 0.01)) // ✓
assertThat(10.1, closeTo(10, 0.01))
// GOT: 10.1 (difference of 0.0999999999999996), EXPECTED: within 0.01 of 10.0

import Foundation
assertThat(CGPoint(x: 5, y: 10), hasProperty("x", closeTo(5.0, 0.00001))) // ✓
assertThat(CGPoint(x: 5, y: 10), hasProperty("y", closeTo(0.0, 0.00001)))
// GOT: (5.0,10.0) (property value 10.0 (difference of 10.0)),
// EXPECTED: has property "y" with value within 1e-05 of 0.0

组合匹配器

Hamcrest 的真正威力来自于将多个匹配器组合成单个断言语句。

assertThat(x, not(equalTo(3))) // ✓
assertThat(x, not(equalTo(2))) // GOT: 2, EXPECTED: not equal to 2

assertThat(x, allOf(greaterThan(1), lessThan(3))) // ✓
assertThat(x, allOf(greaterThan(2), lessThan(3)))
// GOT: 2 (mismatch: greater than 2),
// EXPECTED: all of [greater than 2, greater than 3]

assertThat(x, greaterThan(1) && lessThan(3)) // ✓
assertThat(x, greaterThan(2) && lessThan(3))
// GOT: 2 (mismatch: greater than 2),
// EXPECTED: all of [greater than 2, greater than 3]

assertThat(x, anyOf(greaterThan(2), lessThan(3))) // ✓
assertThat(x, anyOf(greaterThan(2), lessThan(2)))
// GOT: 2, EXPECTED: any of [greater than 2, greater than 2]

assertThat(x, greaterThan(2) || lessThan(3)) // ✓
assertThat(x, greaterThan(2) || lessThan(2))
// GOT: 2, EXPECTED: any of [greater than 2, greater than 2]

集合

组合匹配器对于匹配序列和字典特别有用。

let array = ["foo", "bar"]

assertThat(array, hasCount(2)) // ✓
assertThat(array, hasCount(greaterThan(2)))
// GOT: [foo, bar] (count 2), EXPECTED: has count greater than 2

assertThat(array, everyItem(equalTo("foo")))
// GOT: [foo, bar] (mismatch: bar),
// EXPECTED: a sequence where every item equal to foo

assertThat(array, contains("foo", "bar")) // ✓
assertThat(array, contains(equalTo("foo"), equalTo("bar"))) // ✓
assertThat(array, contains(equalTo("foo")))
// GOT: [foo, bar] (unmatched item "bar"),
// EXPECTED: a sequence containing equal to foo
assertThat(array, contains(equalTo("foo"), equalTo("baz")))
// "GOT: [foo, bar] (mismatch: GOT: "bar", EXPECTED: equal to baz),
// EXPECTED: a sequence containing [equal to foo, equal to baz]"
assertThat(array, contains(equalTo("foo"), equalTo("bar"), equalTo("baz")))
// GOT: [foo, bar] (missing item equal to baz),
// EXPECTED: a sequence containing [equal to foo, equal to bar, equal to baz]

assertThat(array, containsInAnyOrder("bar", "foo")) // ✓
assertThat(array, containsInAnyOrder(equalTo("bar"), equalTo("foo"))) // ✓

assertThat(array, hasItem(equalTo("foo"))) // ✓
assertThat(array, hasItem(equalTo("baz")))
// GOT: [foo, bar], EXPECTED: a sequence containing equal to baz

assertThat(array, hasItem("foo", atIndex: 0))) // ✓
assertThat(array, hasItem("foo", atIndex: 1))) // GOT: ["foo", "bar"], EXPECTED: a sequence containing "foo" at index 1"

assertThat(array, hasItem(equalTo("foo"), atIndex: 0))) // ✓
assertThat(array, hasItem(equalTo("foo"), atIndex: 1))) // GOT: ["foo", "bar"], EXPECTED: a sequence containing "foo" at index 1"


assertThat(array, hasItems("foo", "bar")) // ✓
assertThat(array, hasItems(equalTo("foo"), equalTo("baz")))
// GOT: [foo, bar] (missing item equal to baz),
// EXPECTED: a sequence containing all of [equal to foo, equal to baz]
let dictionary = ["foo": 5, "bar": 10]

assertThat(dictionary, hasEntry("foo", 5)) // ✓
assertThat(dictionary, hasEntry(equalTo("foo"), equalTo(5))) // ✓
assertThat(dictionary, hasEntry(equalTo("foo"), equalTo(10)))
// GOT: [bar: 10, foo: 5],
// EXPECTED: a dictionary containing [equal to foo -> equal to 10]

assertThat(dictionary, hasKey("foo")) // ✓
assertThat(dictionary, hasKey(equalTo("baz")))
// GOT: [bar: 10, foo: 5],
// EXPECTED: a dictionary containing [equal to baz -> anything]

assertThat(dictionary, hasValue(10)) // ✓
assertThat(dictionary, hasValue(equalTo(15)))
// GOT: [bar: 10, foo: 5],
// EXPECTED: a dictionary containing [anything -> equal to 15]

可选类型

匹配器不期望可选类型匹配 Swift 对不可为空类型的偏好。 presentAnd 可以显式地将匹配器应用于可选类型。

var optional: Int = 1 + 1

assertThat(optional, present()) // ✓
assertThat(optional, nilValue()) // GOT: Optional(2), EXPECTED: nil

assertThat(optional, presentAnd(equalTo(2))) // ✓
assertThat(optional, presentAnd(equalTo(1)))
// GOT: Optional(2), EXPECTED: present and equal to 1

类型和转换

以下匹配器可用于断言类型。类型为 Any 的引用需要先进行转换,然后才能使用类型化的匹配器。instanceOf(and:) 可用于组合类型验证和转换。

class TestChild: Test {}
assertThat(o, instanceOf(Test.self)) // ✓
assertThat(o, instanceOf(TestChild.self))
// GOT: __lldb_expr_60.Test, EXPECTED: instance of expected type

let any: Any = 10
assertThat(any, instanceOf(Int.self, and: equalTo(10))) // ✓
assertThat(any, instanceOf(Double.self, and: equalTo(10.0)))
// GOT: 10 (mismatched type), EXPECTED: instance of and equal to 10.0
assertThat(any, instanceOf(Int.self, and: equalTo(5)))
// GOT: 10, EXPECTED: instance of and equal to 5

自定义匹配器

有两种创建自定义匹配器的方法。第一种方法是创建一个简单地返回现有匹配器组合的函数。

func isOnAxis<Point>() -> Matcher<Point> {
    return anyOf(hasProperty("x", closeTo(0.0, 0.00001)),
                 hasProperty("y", closeTo(0.0, 0.00001)))
}

assertThat(CGPoint(x: 0, y: 10), isOnAxis()) // ✓
assertThat(CGPoint(x: 5, y: 10), isOnAxis())
// GOT: (5.0,10.0),
// EXPECTED: any of [has property "x" with value within 1e-05
// of 0.0, has property "y" with value within 1e-05 of 0.0]

您可以使用特殊的匹配器 describedAs 来自定义描述。

func isOnAxis2<Point>() -> Matcher<Point> {
    return describedAs("a point on an axis",
        anyOf(hasProperty("x", closeTo(0.0, 0.00001)),
              hasProperty("y", closeTo(0.0, 0.00001))))
}

assertThat(CGPoint(x: 0, y: 10), isOnAxis2()) // ✓
assertThat(CGPoint(x: 5, y: 10), isOnAxis2())
// GOT: (5.0,10.0), EXPECTED: a point on an axis

第二种方法是从头开始创建一个匹配器。 SwiftHamcrest 特别注重使这种自定义匹配器易于编写。在许多 Hamcrest 实现中,您通常为此创建一个类。在 SwiftHamcrest 中,您只需创建 Matcher 的一个实例,并使用一个自定义闭包,该闭包接受一个值并返回一个 Bool。

func isEven() -> Matcher<Int> {
    return Matcher("even") {$0 % 2 == 0}
}

assertThat(x, isEven()) // ✓
assertThat(3, isEven()) // GOT: 3, EXPECTED: even

虽然 Bool 很方便(并且在大多数情况下都足够),但在某些情况下您需要更多关于不匹配的信息。除了 Bool 之外,您还可以让闭包返回 MatchResult 枚举。如果这种不匹配不明显,则尤其有用。

func isDivisibleByThree() -> Matcher<Int> {
    return Matcher("divisible by three") {
        (value) -> MatchResult in
        if value % 3 == 0 {
            return .Match
        } else {
            return .Mismatch("remainder: \(value % 3)")
        }
    }
}

assertThat(342783, isDivisibleByThree()) // ✓
assertThat(489359, isDivisibleByThree())
// GOT: 489359 (remainder: 2), EXPECTED: divisible by three

错误

如果您正在测试的函数可以抛出错误,Hamcrest 将报告这些错误。

private enum SampleError: Error {
    case Error1
    case Error2
}

private func throwingFunc() throws -> Int {
    throw SampleError.Error1
}

assertThat(try throwingFunc(), equalTo(1)) // ERROR: SampleError.Error1

如果您不想测试可能抛出错误的函数的结果,或者如果此函数未返回任何错误,请使用 assertNotThrows

private func notThrowingFunc() throws {
}

assertNotThrows(try notThrowingFunc()) // ✓
assertNotThrows(_ = try throwingFunc()) // ERROR: UNEXPECTED ERROR

如果您想验证是否抛出了错误,请使用 assertThrows

assertThrows(try notThrowingFunc()) // EXPECTED ERROR
assertThrows(try notThrowingFunc(), SampleError.Error2)
// EXPECTED ERROR: SampleError.Error2

assertThrows(try throwingFunc(), SampleError.Error2)
// GOT ERROR: SampleError.Error1, EXPECTED ERROR: SampleError.Error2

消息

如果匹配器没有给出有意义的消息,您可以添加一个自定义消息,以便在匹配器不匹配时显示该消息。

assertThat(true, equalTo(false), message: "Custom error message")

// failed: 自定义错误消息 – GOT: true, EXPECTED: 等于 false

集成

Swift Package Manager

在您的 Xcode 项目中选择“Add Package Dependency”选项,并将此仓库的 URL 复制到“Choose Package Repository”窗口中。

Swift-Testing

Hamcrest 也适用于新的 Swift-Testing 框架。迁移到 Swift-Testing 非常容易,因为有 #assertThat 宏。该宏位于模块 HamcrestSwiftTesting 中,因此需要导入它。

例如:


import Hamcrest
import HamcrestSwiftTesting
import Testing

struct ExampleTest {

    @Test func test_assertThat() async throws {
        #assertThat("foo", equalTo("foo"))
        #assertThat("foo", not(equalTo("bar")))
    }
}

CocoaPods

使用类似于以下的 Podfile 集成 SwiftHamcrest

use_frameworks!

target 'HamcrestDemoTests' do
  inherit! :search_paths
  pod 'SwiftHamcrest', '~> 2.2.4'
end

Carthage

将以下内容添加到您的 Cartfile

github "nschum/SwiftHamcrest"