如何以声明方式断言/验证/匹配 Kotlin 数据类结构

问题描述 投票:0回答:1

假设我在 Kotlin 中有以下数据结构(根据实际需要进行了简化):

data class RootClass(
    val a: String,
    val nestedContent: MiddleClass,
) : Result

data class MiddleClass(
    val foo: String,
    val leaf: Leaf,
)

data class Leaf(
    val value: String,
    val leaf: Leaf?,
)

出于测试中断言/验证的目的,是否有某种方法(例如使用某些库)以声明性方式定义预期结果,例如允许忽略任意字段。这样预期的结果可以通过以下方式呈现出来

RootClass(
    a = "Foo",
    nestedContent = MiddleClass(
        foo = anyString(),
        leaf = Leaf(
            value = "leaf-value",
            leaf = any(Leaf)
        )
    )
)

我对assertK、Hamcrest 和mockK 匹配器等进行了一些研究,虽然还不是很深入,但还没有找到“最佳方法”。对于典型的用例,通常可以例如创建自定义帮助程序来检查所需的字段,但针对上述内容进行测试有时会很有用且易于阅读。

kotlin automated-tests verification matcher
1个回答
0
投票

如果您想要声明式,类型安全构建器模式非常有用。

首先,声明一个代表匹配器的接口。

interface Matcher<in T> {
    fun match(value: T): Boolean
}

然后您可以为此创建各种实现(例如,请参阅 Hamcrest 中可用的各种匹配器)。现在,我只需要这些简单的:

data class EqualityMatcher<T>(val value: T): Matcher<T> {
    override fun match(value: T) = value == this.value
}

object AnyNonNull: Matcher<Any?> {
    override fun match(value: Any?) = value != null;
}

data class PropertyMatcher<T, V>(
    val property: KProperty1<T, V>,
    val downstream: Matcher<V>
): Matcher<T> {
    override fun match(value: T) = downstream.match(property.get(value))
}

然后我们可以编写一个构建器来构建一种新型匹配器,仅当

Matcher<T>
列表包含所有匹配项时才匹配。

data class ComposedMatcher<T>(val matchers: MutableList<Matcher<T>> = mutableListOf()): Matcher<T> {
    override fun match(value: T) = matchers.all { it.match(value) }

    infix fun <V> KProperty1<T, V>.shouldBe(value: V) {
        matchers.add(PropertyMatcher(this, EqualityMatcher(value)))
    }

    infix fun <V> KProperty1<T, V>.shouldMatch(matcher: Matcher<V>) {
        matchers.add(PropertyMatcher(this, matcher))
    }

    inline infix fun <V> KProperty1<T, V>.shouldMatch(block: ComposedMatcher<V>.() -> Unit) {
        matchers.add(PropertyMatcher(this, match(block)))
    }
}

inline fun <T> match(block: ComposedMatcher<T>.() -> Unit): Matcher<T> =
    ComposedMatcher<T>().apply(block)

仅此而已,我们就可以创建一个像您问题中的那样的

Matcher<RootClass>

val matcher = match {
    RootClass::a shouldBe "Foo"
    RootClass::nestedContent shouldMatch {
        MiddleClass::foo shouldMatch AnyNonNull
        MiddleClass::leaf shouldMatch {
            Leaf::value shouldBe "leaf-value"
            Leaf::leaf shouldMatch AnyNonNull
        }
    }
}

通过对名称进行一些调整,您可以使其更具可读性。

作为扩展,尝试创建一个

anyOf
匹配器,当其匹配器列表中的任何匹配器匹配时进行匹配。

© www.soinside.com 2019 - 2024. All rights reserved.