vlambda博客
学习文章列表

读书笔记《building-applications-with-spring-5-and-kotlin》测试

Testing

为了能够开发出好的软件,编写测试是必须的。编写测试将证明您的代码工作正常,并帮助您检测 QA 团队无法发现的问题。在本章中,我们将指导您完成测试指南,并向您展示如何执行此操作的实际示例。我们将从解释测试的重要性开始,然后继续进行测试中的实际步骤,包括编写和常见的测试实践。

在本章中,我们将讨论以下内容:

  • Why is testing a crucial part of development?
  • Some bad development practices
  • Some good development practices

Why is testing a crucial part of development?

自动化测试是检查您编写的代码是否正常工作的好方法。测试检查我们的软件是否正常工作,是否会继续按照我们预期的方式工作。编写测试有许多不同的方法和实践。在本章中,我们将提到其中的一些。

在 QA 团队开始测试产品之前,开发人员应确保所有类都包含适当的测试,并且所有测试都通过了。当开发团队不关心编写适当的测试或没有用测试覆盖大部分(理想情况下是全部)代码库时,QA 团队很可能会在测试中遇到问题并提出问题。

那么,测试有什么好处呢?首先,被测试覆盖的代码错误更少。此外,它还可以更轻松地跟踪文档,并总体上改进软件设计。

通过使用测试代码,它更容易重用。您可以轻松地在新项目中重用您的测试和代码库。编写测试将提高您对正在处理的代码的理解。

测试的一些主要好处是改进类,包括可见性、解耦和更好的组织,这将使您的代码更易于维护和扩展。为已经包含测试的代码库编写测试将是例行公事。

如果我们在开发阶段编写测试,我们将能够在开发的早期阶段捕获错误,以便在最后阶段,我们将几乎没有错误或根本没有错误。通过测试,我们将获得以下好处:

  • Better visibility
  • Better error reporting
  • Development efficiency
  • Speed
  • Practicability of system behavior
  • Easier planning

So, what is testing?

总而言之,测试代表开发人员通常编写在项目源代码上执行的测试的那种测试。执行的所有测试都是通过了解有关正在测试的源代码的所有信息来完成的。在理想情况下,编写测试的人将对源代码的每个类执行测试。多亏了这一点,我们可以确定我们所有的代码都按照我们的预期运行。

Common test practices and approaches

让我们首先注意所有测试必须满足的一些要点:

  • Every team member must be able to run tests or test collections we created with no significant effort
  • Continuous integration (CI) servers must be able to run these tests out of the box without any need for additional actions
  • Results of testing must be unambiguous
  • Each time we repeat the tests, we must get the same results

我们将区分几种编写测试的方式。编写测试的方法之一是通过测试驱动开发 (TDD)。在 TDD 中,测试是在功能的实现代码之前编写的。测试对您的功能必须满足的功能进行断言,然后我们可以开始开发这些功能。断言将在实现完成之前失败。随着我们实施的进展,每个测试都将通过,直到我们涵盖了所有内容。当我们想要确定系统的需求并允许对我们所有的功能进行回归测试时,通常会使用这种方法,尤其是在完成一些更改的情况下。

一些开发人员倾向于在功能实现后编写测试。通常,在这种情况下,有必要使用一些代码覆盖率工具,以便它们确定测试覆盖的代码百分比。

Preparing our project

在开始编写测试之前,我们将对 build.gradle 配置进行一些更改:

buildscript { 
    ext { 
        kotlinVersion = '1.2.21' 
        springBootVersion = '2.0.0.M4' 
    } 
    repositories { 
        mavenCentral() 
        maven { url "https://repo.spring.io/snapshot" } 
        maven { url "https://repo.spring.io/milestone" } 
    } 
    dependencies { 
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 
    } 
} 
 
apply plugin: 'kotlin' 
apply plugin: 'kotlin-spring' 
apply plugin: 'eclipse' 
apply plugin: 'org.springframework.boot' 
apply plugin: 'io.spring.dependency-management' 
 
group = 'com.journaler' 
version = '0.0.1-SNAPSHOT' 
sourceCompatibility = 1.8 
 
compileKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
compileTestKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
 
repositories { 
    mavenCentral() 
    maven { url "https://repo.spring.io/snapshot" } 
    maven { url "https://repo.spring.io/milestone" } 
} 
 
dependencies { 
    compile 'org.springframework.boot:spring-boot-starter-security' 
    compile 'org.springframework:spring-context' 
    compile 'org.springframework:spring-aop' 
    compile 'org.springframework.boot:spring-boot-starter' 
    compile 'org.springframework.boot:spring-boot-starter-web' 
    compile 'org.springframework.boot:spring-boot-starter-actuator' 
    compile 'org.springframework:spring-web' 
    compile 'org.springframework:spring-webmvc' 
    runtime 'mysql:mysql-connector-java' 
    compile 'org.springframework.boot:spring-boot-starter-data-jpa' 
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}" 
    compile "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}" 
    testCompile 'org.springframework.boot:spring-boot-starter-test' 
} 

我们更新了依赖项以匹配 IntelliJ IDE 中包含的 Kotlin 版本。查看是否有关于此的警告消息,并确保您的 build.gradle 配置遵循您的 IntelliJ IDE 的版本。为了确定我们到底在改变什么,看看这个差异:

读书笔记《building-applications-with-spring-5-and-kotlin》测试
The difference between the previous and the current build.gradle configuration

我们将稍微简化我们的应用程序,以便更容易地演示测试编写过程。我们将移除 Reactor 并应用一些更改。删除以下文件:

  • bootsrtap.properties (we don't want complex configuration at the moment)
  • MailMessage.kt
  • MailService.kt
  • MailServiceImpl.kt
  • NotesCountNotification.kt
  • NotesCountNotificationConsumer.kt
  • NotesCountNotificationService.kt
  • NotesCountNotificationServiceImpl.kt
  • NotificationConsumer.kt
  • NotificationService.kt
  • SessionConfiguration.kt
  • TodosCountNotification.kt
  • TodosCountNotificationConsumer.kt
  • TodosCountNotificationService.kt
  • TodosCountNotificationServiceImpl.kt

然后,再次创建 application.properties 文件:

spring.application.name= journaler 
server.port= 9000 
logging.level.root=INFO 
logging.level.com.journaler.api=DEBUG 
logging.level.org.springframework.jdbc=ERROR 
 
endpoints.health.enabled=true 
endpoints.trace.enabled=true 
endpoints.info.enabled=true 
endpoints.metrics.enabled=true 
 
spring.datasource.url=jdbc:mysql://localhost/journaler_api?useSSL=false&useUnicode=true&characterEncoding=utf-8 
spring.datasource.username=root 
spring.datasource.password=localInstance2017 
spring.datasource.tomcat.test-on-borrow=true 
spring.datasource.tomcat.validation-interval=30000 
spring.datasource.tomcat.validation-query=SELECT 1 
spring.datasource.tomcat.remove-abandoned=true 
spring.datasource.tomcat.remove-abandoned-timeout=10000 
spring.datasource.tomcat.log-abandoned=true 
spring.datasource.tomcat.max-age=1800000 
spring.datasource.tomcat.log-validation-errors=true 
spring.datasource.tomcat.max-active=50 
spring.datasource.tomcat.max-idle=10 
 
spring.jpa.hibernate.ddl-auto=update 

像这样更新 ApiApplication 类:

package com.journaler.api 
 
import org.springframework.boot.SpringApplication 
import org.springframework.boot.autoconfigure.SpringBootApplication 
 
@SpringBootApplication 
class ApiApplication 
 
fun main(args: Array<String>) { 
    SpringApplication.run(ApiApplication::class.java, *args) 
} 

并相应地更新服务:

  • NoteService:
package com.journaler.api.service 
 
import com.journaler.api.data.Note 
import com.journaler.api.data.NoteDTO 
import com.journaler.api.repository.NoteRepository 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.stereotype.Service 
import java.util.* 
 
@Service("Note service") 
class NoteService { 
 
    @Autowired 
    lateinit var repository: NoteRepository 
 
    fun getNotes(): Iterable<NoteDTO> = repository.findAll().map { it -> NoteDTO(it) } 
 
    fun insertNote(note: NoteDTO) = NoteDTO( 
            repository.save( 
                    Note( 
                            title = note.title, 
                            message = note.message, 
                            location = note.location 
                    ) 
            ) 
    ) 
 
    fun deleteNote(id: String) = repository.deleteById(id) 
 
    fun updateNote(noteDto: NoteDTO): NoteDTO { 
        val note = repository.findById(noteDto.id).get() 
        note.title = noteDto.title 
        note.message = noteDto.message 
        note.location = noteDto.location 
        note.modified = Date() 
        return NoteDTO(repository.save(note)) 
    } 
 
    fun findByTitle(title: String): Iterable<NoteDTO> { 
        return repository.findByTitle(title).map { it -> NoteDTO(it) }
    } 
 
} 
  • TodoService:
package com.journaler.api.service 
 
import com.journaler.api.data.Todo 
import com.journaler.api.data.TodoDTO 
import com.journaler.api.repository.TodoRepository 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.stereotype.Service 
import java.util.* 
 
 
@Service("Todo service") 
class TodoService { 
 
    @Autowired 
    lateinit var repository: TodoRepository 
 
    fun getTodos(): Iterable<TodoDTO> = repository.findAll().map { it -> TodoDTO(it) } 
 
    fun insertTodo(todo: TodoDTO) = TodoDTO( 
            repository.save( 
                    Todo( 
                            title = todo.title, 
                            message = todo.message, 
                            location = todo.location, 
                            schedule = todo.schedule 
 
                    ) 
            ) 
    ) 
 
    fun deleteTodo(id: String) = repository.deleteById(id) 
 
    fun updateTodo(todoDto: TodoDTO): TodoDTO { 
        val todo = repository.findById(todoDto.id).get() 
        todo.title = todoDto.title 
        todo.message = todoDto.message 
        todo.location = todoDto.location 
        todo.schedule = todoDto.schedule 
        todo.modified = Date() 
        return TodoDTO(repository.save(todo)) 
    } 
 
    fun getScheduledLaterThan(date: Date?): Iterable<TodoDTO> { 
        date?.let { 
            return repository.findScheduledLaterThan(date.time).map { it -> TodoDTO(it) } 
        } 
        return listOf() 
    } 
 
} 
 

Writing our first test with Kotlin

在本节中,我们将通过创建几个简单的测试来慢慢介绍测试编写。找到您的 src/test 目录,并在 com.journaler 包下,创建一个名为 NoteTest 的新类:

package com.journaler 
 
import org.junit.Test 
 
class NoteTest { 
 
    @Test 
    fun testNoteInsert(){ 
    } 
 
} 

我们刚刚定义了一个空测试,此时它什么都不做。尽管如此,我们还是介绍了一些非常重要的东西。我们使用了 @Test 注释,它将告诉测试框架哪个方法将代表测试实现。我们不限于每个测试类只有一个测试,所以让我们在测试类中添加更多测试:

package com.journaler 
 
import org.junit.Test 
 
class NoteTest { 
    @Test 
    fun testNoteInsert(){ 
    } 
 
    @Test 
    fun testNoteUpdate(){ 
    } 
 
    @Test 
    fun testNoteDelete(){ 
    } 
 
    @Test 
    fun testNoteSelect(){ 
    } 
} 

现在,我们的 NoteTest 类中有四个测试,没有实际实现。我们知道每个方法都会实际测试一些东西,我们不需要在每个方法的名称中使用这个 prefix

package com.journaler 
 
import org.junit.Test 
 
class NoteTest { 
    @Test 
    fun noteInsert(){ 
    } 
 
    @Test 
    fun noteUpdate(){ 
    } 
 
    @Test 
    fun noteDelete(){ 
    } 
 
    @Test 
    fun noteSelect(){ 
    } 
} 

实际上,我们也不需要 note 前缀,因为 NoteTest 类将专用于仅测试 Note 实体:

package com.journaler 
 
import org.junit.Test 
 
class NoteTest { 
    @Test 
    fun insert(){ 
    } 
 
    @Test 
    fun update(){ 
    } 
 
    @Test 
    fun delete(){ 
    } 
 
    @Test 
    fun select(){ 
    } 
} 

现在,假设我们对准备运行这四个测试的环境有一些要求,并且在每个测试完成后进行额外的清理。我们需要引入两个新的注解:

package com.journaler 
 
import org.junit.After 
import org.junit.Before 
import org.junit.Test 
 
class NoteTest { 
    @Before 
    fun prepare(){ 
        // Prepare environment and requirements for tests to be performed. 
    } 
 
    @Test 
    fun insert(){ 
        // Test insert operation for Note entity. 
    } 
 
    @Test 
    fun update(){ 
        // Test update operation for Note entity. 
    } 
 
    @Test 
    fun delete(){ 
        // Test delete operation for Note entity. 
    } 
 
    @Test 
    fun select(){ 
        // Test select operation for Note entity. 
    } 
 
    @After 
    fun cleanup(){ 
        // Do cleanup after all tests are performed. 
    } 
} 

如您所见,我们引入了 @Before@After 注释。这些标记将在这四个测试之前和之后执行的方法。这意味着当执行每个测试时,我们将触发 prepare()cleanup() 方法。不保证测试方法调用顺序!那么,如果我们运行几次测试会发生什么?为了得到这个问题的答案,我们需要在我们的测试中加入一些输出:

package com.journaler 
 
import org.junit.After 
import org.junit.Before 
import org.junit.Test 
 
class NoteTest { 
    @Before 
    fun prepare() { 
        println("Prepare.") 
        // Prepare environment and requirements for tests to be performed. 
    } 
 
    @Test 
    fun insert() { 
        println("Insert.") 
        // Test insert operation for Note entity. 
    } 
 
    @Test 
    fun update() { 
        println("Update.") 
        // Test update operation for Note entity. 
    } 
 
    @Test 
    fun delete() { 
        println("Delete.") 
        // Test delete operation for Note entity. 
    } 
 
    @Test 
    fun select() { 
        println("Select.") 
        // Test select operation for Note entity. 
    } 
 
    @After 
    fun cleanup() { 
        println("Cleanup.") 
        // Do cleanup after all tests are performed. 
    } 
} 

运行测试三遍。让我们看看我们得到了什么输出:

  • With iteration 1, we get the following:
    • Prepare
    • Delete
    • Cleanup
    • Prepare
    • Insert
    • Cleanup
    • Prepare
    • Select
    • Cleanup
    • Prepare
    • Update
    • Cleanup
  • With iteration 2, we get the following:
    • Prepare
    • Delete
    • Cleanup
    • Prepare
    • Insert
    • Cleanup
    • Prepare
    • Select
    • Cleanup
    • Prepare
    • Update
    • Cleanup
  • With iteration 3, we get the following:
    • Prepare
    • Delete
    • Cleanup
    • Prepare
    • Insert
    • Cleanup
    • Prepare
    • Select
    • Cleanup
    • Prepare
    • Update
    • Cleanup

如您所见,每次的顺序都是相同的,但不是按照我们在 NoteTest 类中定义的顺序。让我们对此做点什么。像这样更新您的 NoteTest 类实现:

package com.journaler 
 
import org.junit.After 
import org.junit.Before 
import org.junit.Test 
 
class NoteTest { 
    @Before 
    fun prepare() { 
        println("Prepare.") 
        // Prepare environment and requirements for tests to be performed. 
    } 
 
    @Test 
    fun crud() { 
        // Test Note entity CRUD operations. 
        insert() 
        update() 
        delete() 
        select() 
    } 
 
    @After 
    fun cleanup() { 
        println("Cleanup.") 
        // Do cleanup after all tests are performed. 
    } 
 
    fun insert() { 
        println("Insert.") 
        // Test insert operation for Note entity. 
    } 
 
    fun update() { 
        println("Update.") 
        // Test update operation for Note entity. 
    } 
 
    fun delete() { 
        println("Delete.") 
        // Test delete operation for Note entity. 
    } 
 
    fun select() { 
        println("Select.") 
        // Test select operation for Note entity. 
    } 
} 

现在,我们有一个测试将按照我们需要的顺序触发测试步骤。运行此测试将为我们提供以下输出:

  • Prepare
  • Insert
  • Update
  • Delete
  • Select
  • Cleanup

请注意,现在我们只调用了一次 prepare()cleanup() 方法。

Running the test in InteliJ IDEA

让我们在 IntelliJ IDEA 中运行这个测试。按着这些次序:

  1. Locate the test file in the hierarchy as shown in the following screenshot:
读书笔记《building-applications-with-spring-5-and-kotlin》测试
  1. Right-click on it and choose Run 'NoteTest', as shown in the following screenshot:
读书笔记《building-applications-with-spring-5-and-kotlin》测试
  1. The test will be executed. Wait while it finishes and observes the test results, as shown in the following screenshot:
读书笔记《building-applications-with-spring-5-and-kotlin》测试
  1. IntelliJ IDEA automatically creates the configuration for you. So, if you expand the available configuration at the top of the window, you will see NoteTest configuration, as shown in the following screenshot:
读书笔记《building-applications-with-spring-5-and-kotlin》测试
  1. Click on it to choose the configuration and then click on the Run button to run the test once more.

Testing Spring REST applications

在本节中,我们将继续测试实现,并将测试特定于 Spring 的主题。首先,我们将指定 SprintRunner 类,它将用作我们测试的测试运行器:

package com.journaler 
 
import org.junit.After 
import org.junit.Before 
import org.junit.Test 
import org.junit.runner.RunWith 
import org.springframework.test.context.junit4.SpringRunner 
 
@RunWith(SpringRunner::class) 
class NoteTest { 
    ... 
} 

通过注释 @RunWith(SpringRunner::class),我们指定 SpringRunner 将用于测试运行并将初始化 TestContextManager 以提供 Spring测试功能到标准测试。让我们再次运行测试。观察以下屏幕截图中的输出:

读书笔记《building-applications-with-spring-5-and-kotlin》测试

如您所见,Spring Framework 被用于支持我们刚刚运行的测试。

我们已经知道,NoteTest 类没有任何实现。我们将测试我们的应用程序的服务层,我们将添加一些代码,以便测试可以实际检查一些东西。我们将注入 NoteService 并断言它已被实例化:

package com.journaler 
 
import com.journaler.api.ApiApplication 
import com.journaler.api.data.NoteDTO 
import com.journaler.api.service.NoteService 
import org.junit.After 
import org.junit.Assert 
import org.junit.Before 
import org.junit.Test 
import org.junit.runner.RunWith 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.boot.test.context.SpringBootTest 
import org.springframework.test.context.junit4.SpringRunner 
 
@RunWith(SpringRunner::class) 
@SpringBootTest(classes = [ApiApplication::class]) 
class NoteTest { 
 
    @Autowired 
    private lateinit var service: NoteService 
 
    @Before 
    fun prepare() { 
        println("Prepare.") 
        Assert.assertNotNull(service) 
    } 
 
    @Test 
    fun crud() { 
        // Test Note entity CRUD operations. 
        insert() 
        update() 
        delete() 
        select() 
    } 
 
    @After 
    fun cleanup() { 
        println("Cleanup.") 
        // Do cleanup after all tests are performed. 
    }
    fun insert() { 
        println("Insert.") 
    } 
 
    fun update() { 
        println("Update.") 
        // Test update operation for Note entity. 
    } 
 
    fun delete() { 
        println("Delete.") 
    } 
 
    fun select() { 
        println("Select.") 
        // Test select operation for Note entity. 
    } 
} 

我们成功地注入了 NoteService 类并断言它。我们还介绍了一个非常重要的注解:@SpringBootTest。可以在运行基于 Spring Boot 的测试的测试类上指定此注解。正如类文档所指定的,它在常规 Spring TestContext Framework 上提供了以下功能:

  • Uses SpringBootContextLoader as the default ContextLoader when no specific @ContextConfiguration(loader=...) is defined
  • Automatically searches for a @SpringBootConfiguration when the nested @Configuration is not used, and no explicit classes are specified
  • Allows custom environment properties to be defined using the properties attribute
  • Provides support for different web environment modes, including the ability to start a fully running container listening on a defined or random port
  • Registers a TestRestTemplate bean for use in web tests that are using a fully running container

运行你的测试;它必须通过,如以下屏幕截图所示:

读书笔记《building-applications-with-spring-5-and-kotlin》测试

我们现在拥有了测试服务层的笔记所需的一切。让我们编写一些测试实现来验证服务层的功能:

package com.journaler 
 
import com.journaler.api.ApiApplication 
import com.journaler.api.data.NoteDTO 
import com.journaler.api.service.NoteService 
import org.junit.After 
import org.junit.Assert 
import org.junit.Before 
import org.junit.Test 
import org.junit.runner.RunWith 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.boot.test.context.SpringBootTest 
import org.springframework.test.context.junit4.SpringRunner 
 
@RunWith(SpringRunner::class) 
@SpringBootTest(classes = [ApiApplication::class]) 
class NoteTest { 
 
    @Autowired 
    private lateinit var service: NoteService 
 
    private val notes = mutableListOf<NoteDTO>() 
 
    @Before 
    fun prepare() { 
        Assert.assertNotNull(service) 
        // We will prepare 10 Note instances to be inserted: 
        (0..10).mapTo(notes) { 
            NoteDTO( 
                    "Stub note title: $it", 
                    "Stub note message: $it" 
            ) 
        } 
    } 
 
    @Test 
    fun crud() { 
        // Test Note entity CRUD operations. 
        cleanup()   // We will empty database before run test. 
        insert()    // We will insert all prepared Note instances into database. 
        update()    // We will update each Note. 
        select()    // We will verify that saved Note instances are valid. 
        delete()    // We will remove all Note instances from database. 
    } 
 
    fun cleanup() { 
        service.getNotes().forEach { note -> 
            service.deleteNote(note.id) 
        } 
    } 
 
    fun insert() { 
        notes.forEach { note -> 
            val result = service.insertNote(note) 
            Assert.assertNotNull(result) 
            Assert.assertNotNull(result.id) 
            Assert.assertFalse(result.id.isEmpty()) 
            note.id = result.id 
        } 
    } 
 
    fun update() { 
        notes.forEach { note -> 
            note.title = "updated" 
            note.message = "updated" 
            val result = service.updateNote(note) 
            Assert.assertNotNull(result) 
            Assert.assertNotNull(result.id) 
            Assert.assertFalse(result.id.isEmpty()) 
            Assert.assertEquals("updated", result.title) 
            Assert.assertEquals("updated", result.message) 
        } 
    } 
 
    fun delete() { 
        notes.forEach { note -> 
            println("Removing note with id: ${note.id}") 
            service.deleteNote(note.id) 
        } 
    } 
 
    fun select() { 
        val result = service.getNotes() 
        result.forEach { note -> 
            Assert.assertNotNull(note) 
            Assert.assertNotNull(note.id) 
            Assert.assertFalse(note.id.isEmpty()) 
            Assert.assertEquals("updated", note.title) 
            Assert.assertEquals("updated", note.message) 
        } 
    } 
} 

在这里,我们稍微重新组织了代码,使用 cleanup 方法在测试运行之前清理数据,而不是在测试之后调用它。我们还切换了 select()delete() 方法的方法执行顺序。每个方法的实现应该很容易理解。我们将为 Note 实体执行和验证 crud 操作。然后,运行您的测试。它会成功:

读书笔记《building-applications-with-spring-5-and-kotlin》测试

我们将为 TODO 编写一个类似的实现。创建 TodoTest 类并确保它是这样实现的:

package com.journaler 
 
import com.journaler.api.ApiApplication 
import com.journaler.api.data.TodoDTO 
import com.journaler.api.service.TodoService 
import org.junit.Assert 
import org.junit.Before 
import org.junit.Test 
import org.junit.runner.RunWith 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.boot.test.context.SpringBootTest 
import org.springframework.test.context.junit4.SpringRunner 
 
@RunWith(SpringRunner::class) 
@SpringBootTest(classes = [ApiApplication::class]) 
class TodoTest { 
 
    @Autowired 
    private lateinit var service: TodoService 
 
    private val todos = mutableListOf<TodoDTO>() 
 
    @Before 
    fun prepare() { 
        Assert.assertNotNull(service) 
        // We will prepare 10 instances to be inserted: 
        (0..10).mapTo(todos) { 
            TodoDTO( 
                    "Stub todo title: $it", 
                    "Stub todo message: $it", 
                    System.currentTimeMillis() 
            ) 
        } 
    } 
 
    @Test 
    fun crud() { 
        // Test entity CRUD operations. 
        cleanup()   // We will empty database before run test. 
        insert()    // We will insert all prepared instances into database. 
        update()    // We will update each item. 
        select()    // We will verify that saved instances are valid. 
        delete()    // We will remove all instances from database. 
    } 
 
    fun cleanup() { 
        service.getTodos().forEach { todo -> 
            service.deleteTodo(todo.id) 
        } 
    } 
 
    fun insert() { 
        todos.forEach { todo -> 
            val result = service.insertTodo(todo) 
            Assert.assertNotNull(result) 
            Assert.assertNotNull(result.id) 
            Assert.assertFalse(result.id.isEmpty()) 
            Assert.assertTrue(result.schedule > 0) 
            todo.id = result.id 
        } 
    } 
 
    fun update() { 
        todos.forEach { todo -> 
            todo.title = "updated" 
            todo.message = "updated" 
            val result = service.updateTodo(todo) 
            Assert.assertNotNull(result) 
            Assert.assertNotNull(result.id) 
            Assert.assertFalse(result.id.isEmpty()) 
            Assert.assertEquals("updated", result.title) 
            Assert.assertEquals("updated", result.message) 
            Assert.assertTrue(result.schedule > 0) 
        } 
    } 
 
    fun delete() { 
        todos.forEach { todo -> 
            println("Removing todo with id: ${todo.id}") 
            service.deleteTodo(todo.id) 
        } 
    } 
 
    fun select() { 
        val result = service.getTodos() 
        result.forEach { todo -> 
            Assert.assertNotNull(todo) 
            Assert.assertNotNull(todo.id) 
            Assert.assertFalse(todo.id.isEmpty()) 
            Assert.assertEquals("updated", todo.title) 
            Assert.assertEquals("updated", todo.message) 
            Assert.assertTrue(todo.schedule > 0) 
        } 
    } 
 
} 

我们做了与 Note 实体相同的测试,除了我们验证了特定于 TODO 实体的 schedule 字段。如果您愿意,可以扩展这两个测试以验证更多属性。运行 TodoTest

读书笔记《building-applications-with-spring-5-and-kotlin》测试

测试将成功执行。

Running test suites

想象一下,您继续编写测试并为更多实体编写它们。一个一个地运行它们可能会令人沮丧。为了让我们的生活更轻松,我们将创建一个 container 测试,其中将包含所有这些。创建一个名为 SuiteTest 的新类并像这样实现它:

package com.journaler 
 
import org.junit.runner.RunWith 
import org.junit.runners.Suite 
 
@RunWith(Suite::class) 
@Suite.SuiteClasses(NoteTest::class, TodoTest::class) 
class SuiteTest 

这个实现很容易理解,但我们将重点介绍最重要的部分。 @Suite.SuiteClasses(NoteTest::class, TodoTest::class) 注释将定义我们将执行的测试。这是我们想要执行的所有测试类的列表。 @RunWith(Suite::class) 表示此测试将作为测试套件执行。现在,运行测试套件。观察;我们执行了两个测试:

读书笔记《building-applications-with-spring-5-and-kotlin》测试

Summary

我们在本章中所做的练习只是进入测试世界的漫长旅程的开始,尤其是在测试特定于 Spring 的特性时。 Spring Framework 为我们提供了能够测试其每个组件的工具。为了更好地理解 Spring Framework 测试的概念和能力,我们建议您尽可能阅读官方的 Spring 测试文档或阅读专门针对该领域的书籍。现在,了解我们在这里介绍的内容、测试本身的概念以及为什么它对我们如此重要就足够了。在下一章中,我们将部署我们的项目并准备、构建和演示部署过程。这是剩下的最后一块拼图!