使用Spock在Kotlin中进行测试

本文的目的是说明将Spock与Kotlin一起使用时会遇到什么困难,有哪些解决方法,并回答如果在Kotlin上进行开发,是否值得使用Spock。细节剪下。

我在一家从事极限编程的公司工作。我们在日常工作中使用的极限编程的主要方法之一是TDD(测试驱动的开发)。这意味着在更改代码之前,我们编写了涵盖所需更改的测试。因此,我们定期编写测试,并且代码覆盖率接近100%。这对选择测试框架提出了一些要求:每周编写一次测试是一回事,每天要做一次测试是另一回事。

我们在Kotlin上进行开发,在某些时候,我们选择了Spock作为主要框架。从那一刻起已经过去了大约6个月,一种欣快感和新颖性已经过去了,所以本文是一种回顾,我将尝试告诉您这段时间我们遇到了什么困难以及如何解决这些困难。

为什么是Spock?




首先,您需要弄清楚哪些框架可以测试Kotlin,以及Spock与它们相比有哪些优势。

Kotlin的优点之一是与Java的兼容性,它允许您使用任何Java框架进行测试,例如JunitTestNGSpock等。同时,还有专门为Kotlin设计的框架,例如SpekKotest。我们为什么选择Spock?

我将强调以下优点:

  • 首先,Spock是用Groovy编写的。是好是坏-自己判断。关于Groovy,您可以在这里阅读文章Groovy亲自将我吸引到简洁的语法,动态类型的存在,列表的内置语法,常规和关联数组以及完全疯狂的类型转换。
  • 其次,Spock已经包含一个模拟框架(MockingApi)和一个断言库。
  • Spock还非常适合编写参数化测试:

def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c

    where:
    a | b | c
    1 | 3 | 3
    7 | 4 | 7
    0 | 0 | 0
  }

这是一个非常肤浅的比较(如果可以称之为比较),但这大致就是我们选择Spock的原因。自然,在接下来的几周和几个月中,我们遇到了一些困难。

用Spock测试Kotlin并解决它们时遇到的问题


在这里,我立即保留一个意见,即以下描述问题并非Spock所独有,它们与任何Java框架都有关,并且主要与与Kotlin的兼容性有关

1.默认情况下为final


问题

与Java不同,默认情况下,所有Kotlin类都有一个修饰符final,可以防止进一步的后代和方法重写。这里的问题是,在创建任何类的模拟对象时,大多数模拟框架都会尝试创建一个代理对象,该对象只是原始类的后代并覆盖其方法。

因此,如果您有服务:

class CustomerService {
    fun getCustomer(id: String): Customer {
        // Some logic
    }
}

并且您尝试在Groovy中创建此服务的模拟:

def customerServiceMock = Mock(CustomerService)

那么你会得到一个错误:
org.spockframework.mock.CannotCreateMockException: Cannot create mock for class CustomerService because Java mocks cannot mock final classes

决定:

  • , , open, . «» , , , , ;
  • all-open . . Spring Framework Hibernate, cglib(Code Generation Library);
  • , mock-, Kotlin (, mockk). , , Spock Spock MockingApi.

2.


问题

在Kotlin中,函数或构造函数参数可以具有默认值,如果在调用时未指定函数参数,则使用默认值。困难在于Java字节码丢失了默认参数值和函数参数名,因此,当从Spock调用Kotlin函数或构造函数时,必须明确指定所有值。

考虑一个Customer具有构造函数的类,该构造函数具有2个必填字段email以及name具有默认值的可选字段:

data class Customer(
    val email: String,
    val name: String,
    val surname: String = "",
    val age: Int = 18,
    val identifiers: List<NationalIdentifier> = emptyList(),
    val addresses: List<Address> = emptyList(),
    val paymentInfo: PaymentInfo? = null
)

为了在Groovy Spock测试中创建此类的实例,您必须将所有参数的值传递给构造函数:

new Customer("john.doe@gmail.com", "John", "", 18, [], [], null)

决定:


class ModelFactory {
    static def getCustomer() { 
        new Customer(
            "john.doe@gmail.com", 
            "John", 
            "Doe", 
            18, 
            [nationalIdentifier], 
            [address], 
            paymentInfo
        ) 
    }

    static def getAddress() { new Address(/* arguments */) }

    static def getNationalIdentifier() { new NationalIdentifier(/* arguments */) }

    static def getPaymentInfo() { new PaymentInfo(/* arguments */) }
}

3. Java Reflection vs Kotlin Reflection


这可能是一个相当狭窄的区域,但是如果您打算在测试中使用反射,则应特别考虑是否使用Groovy。在我们的项目中,我们使用了很多注释,并且为了避免忘记用注释来注释某些类或某些字段,我们使用反射来编写测试。这就是出现困难的地方。

问题

类和注释用Kotlin编写,与测试和反射相关的逻辑用Groovy编写。因此,测试会从Java Reflection API和Kotlin Reflection中产生一个哈希:

将Java类转换为Kotlin:

def kotlinClass = new KClassImpl(clazz)

调用Kotlin扩展功能是静态方法:
ReflectJvmMapping.getJavaType(property.returnType)

使用Kotlin类型的难读代码:
private static boolean isOfCollectionType(KProperty1<Object, ?> property) {
    // Some logic
}

当然,这不是过度或不可能的事情。唯一的问题是,使用Kotlin测试框架是否更容易做到这一点?在这里可以使用扩展功能和纯Kotlin反射?

4.协程


如果您打算使用协程,那么这是考虑是否要选择Kotlin框架进行测试的另一个原因。

问题

如果创建suspend函数:

suspend fun someSuspendFun() {
    // Some logic
}

,则编译如下:

public void someSuspendFun(Continuation<? super Unit> $completion) {
    // Some logic
}

Continuation是一个Java类,其中封装了用于执行协程的逻辑。这是出现问题的地方,如何创建此类的对象?另外,Groovy中不提供runBlocking因此,通常不清楚如何在Spock中使用协同程序来测试代码。

摘要


使用Spock超过六个月,它给我们带来的好处多于坏事,我们对此选择不感到遗憾:测试易于读取和维护,编写参数化测试是一种乐趣。在我们的案例中,药膏中的蝇被证明是反射,但仅在少数地方使用,与协程相同。

选择用于测试Kotlin上正在开发的项目的框架时,应仔细考虑计划使用哪些语言功能。如果您正在编写一个简单的CRUD应用程序,那么Spock是完美的解决方案。您使用协程和反射吗?然后最好看看SpekKotest

该材料并不声称是完整的,因此,如果您在使用Spock和Kotlin时遇到其他困难,请在评论中写下。

有用的链接



All Articles