如何使用 Kotlin 练习 TDD?
1. 你喜欢写测试吗?
2. 您在开发过程中是否实践过测试驱动设计/开发 (TDD)?
你们中的大多数人,包括我在内,我认为测试对于开发人员来说是一种奢侈,因为这是我们在尝试构建最小可行产品时牺牲的第一个任务。
但我们这样做是错误的,因为测试:
减少可能的生产回归
故障排除时什么可以节省我们的时间
验证功能设计
确保我们尊重目标解决方案
提高程序的整体质量
该程序更可靠,我们更有信心在生产中推出新版本
好吧,我们同意做更多的测试,但 TDD ......它是否适用于日常实践?
作为测试驱动开发的 TDD
测试驱动开发是指一种在编写生产代码之前有利于测试的编程风格。基本上,您必须遵循以下步骤:
写测试
运行测试(测试应该失败)
编写生产代码
运行测试(测试应该通过)
重构直到代码符合
随着时间的推移重复、“积累”单元测试
实现TDD的方法有很多,比如单元测试、特性测试等。但是,在实践中,这并不是很自然,我们必须自己努力才能真正使用它。
用于 TDD 的 Kotlin?
Kotlin 在编写 TDD 方面比 Java 有很多优势。特别:
Kotlin 通过功能扩展减少了 java 样板文件,这可以提高您的测试可读性。
Kotlin 支持中缀表示法,允许用更自然的英语编写代码。
val canOrder = 域名thenticated and 域名CreditCard
Kotlin 支持反引号函数名,写测试真的很方便
class MyTestCase { @Test fun `ensure everything works`() { /*...*/ } @Test fun ensureEverythingWorks_onAndroid() { /*...*/ } }
Kotlin 可与 Java 互操作,这意味着您可以使用 Kotlin 编写测试,而您的生产代码是用 Java 编写的。
这就是为什么我认为 Kotlin 是实现 TDD 的最佳语言。我想到了构建一个库来设置 TDD 环境:
@Test fun `I should be able to insert a new item in my todo list`() { given { `a todo list` } and { `an item`("Eat banana") } `when` { `I add the last item into my todo list` } then { `I expect this item is present in my todo list` } }
看起来很酷,对吧?
实际上,Kotlin-TDD 提供了两种风格:
GivenWhenThen露出given,and,when和then缀功能。
AssumeActAssert露出assume,and,act和assert缀功能。
实际上,可以按照 AAA 模式编写上述相同的测试。
@Test fun `I should be able to insert a new item in my todo list`() { assume { `a todo list` } and { `an item`("Eat banana") } act { `I add the last item into my todo list` } assert { `I expect this item is present in my todo list` } }
让我们尝试在一个具体的例子中使用它。
假设您需要创建一个待办事项列表应用程序,并且接受标准之一是。作为用户,当我的新项目被添加到我的待办事项列表时,我应该会看到它。
由于这个库,我们将尝试练习 TDD。
1 - 设置 Kotlin-TDD
让我们首先将此依赖项导入到您的项目中。我假设您的项目中安装了 Junit 5。
使用摇篮:
常规1
testCompile "域名rival:kotlin-tdd:1.1.2"
使用 Maven:XML文件
<dependency> <groupId>域名rival</groupId> <artifactId>kotlin-tdd</artifactId> <version>1.1.2</version> <scope>test</scope> </dependency>
2 - 编写您的界面操作
在每个步骤中,您可以访问名为 action 的字段,该字段是您将传递给 TDD 配置的实例(参见下一步)。它在库中没有真正的用途,但它允许您在整个测试中使用公共实例。在这里,我们将使用它来公开应用程序中不同的可能操作:
// src/test/kotlin/com/example/kotlintdd/域名 package 域名intdd interface Action { fun createTodoList(): TodoList fun createItem(name: String): Item fun addItem(todoList: TodoList, item: Item): TodoList }
如果您看到编译错误,请不要担心,这是 TDD 过程的一部分;)。事实上,编译失败被认为是 TDD 中的测试失败。
3 - 配置 Givenwhenthen 实例以在我们所有的单元测试中使用
// src/test/kotlin/com/example/kotlintdd/域名 package 域名intdd import 域名域名ontext // it is an alias of 域名ext import 域名域名nWhenThen object UnitTest : GivenWhenThen<Action> { override val action: Action = object: Action { override fun createTodoList() = TodoList() override fun createItem(name: String) = Item(name) override fun addItem(todoList: TodoList, item: Item) = 域名(item) } } // defines the entrypoint on file-level to be automatically recognized by your IDE fun <R> given(block: GWTContext<Action, Unit>.() -> R) = 域名n(block) fun <R> `when`(block: GWTContext<Action, Unit>.() -> R) = UnitTest.`when`(block)
4 - 编写您的自定义 DSL
此步骤是可选的,但它有助于以自然语言描述您的操作。在这里,我们将创建一个文件来托管此 DSL。
// src/test/kotlin/com/example/kotlintdd/域名 package 域名intdd import 域名域名ontext val GWTContext<*, *>.`a todo list` get() = 域名teTodoList() fun GWTContext<*, *>.`an item`(name: String) = 域名teItem(name) val GWTContext<*, *>.`I add the last item into my todo list` get() = 域名tem(first<TodoList>(), last<Item>()) val GWTContext<*, *>.`I expect this item is present in my todo list` get() = assertTrue(first<TodoList>().contains(last<Item>()))
在这里,我们通过添加我们自己的自定义 DSL 来扩展 域名ext。请注意,您可以访问 action 实例,这允许我们调用我们各自的 action。
在第 11 和 14 行检查first<TodoList>()和last<Item>()函数的使用。这些函数随 Context 一起提供并存储每个步骤的所有先前上下文:
在first<TodoList>允许获取第TodoList一个步骤中返回的实例。
将last<Item>()允许获得最后Item通过一步返回的实例。
请注意TodoList,Item可以在第 11 行删除,因为它是推断出来的,给定域名tem(first(), last())。
5 - 编写单元测试
现在我们已经配置了我们的 TDD 和我们的自定义 DSL,让我们把它们放在一起测试:
// src/test/kotlin/com/example/kotlintdd/TodoListTest package 域名intdd import 域名域名 class TodoListTest { @Test fun `I should be able to insert a new item in my todo list`() { given { `a todo list` } and { `an item`("Eat banana") } `when` { `I add the last item into my todo list` } then { `I expect this item is present in my todo list` } } }
6 - 运行测试
当然,由于编译错误,测试失败。我们没有编写任何生产代码。别担心,这是 TDD 过程的一部分。
7 - 编写生产代码
现在是时候让测试变绿了。
创建项目类:
// src/main/kotlin/com/example/kotlintdd/Item package 域名intdd data class Item(val name: String)
数据 类 Item ( val name : String )
创建 TodoList 类:
// src/main/kotlin/com/example/kotlintdd/TodoList package 域名intdd class TodoList { val list = mutableListOf() fun add(item: Item) { 域名(item) return this } fun contains(item: Item) = 域名ains(item) }
8 - 再次运行测试
太棒了,你的测试是绿色的!
9 - 后续步骤:验收测试
我们可以通过结合 Given When Then 模式和我们的自定义 DSL 来继续添加更多测试。可以针对更多用例增强 DSL。Kotlin-TDD 的优点是您也可以重用相同的过程来编写验收测试。
假设您有一个Spring 应用程序,其中 TodoList 和 Item 保存在数据库中。应通过这些实体的数据库完成创建和更新。
我们希望在我们的 Rest API 中有三个端点:
POST /v1/todo/list // Create a new Todo list -> return the TodoList with an id POST /v1/todo/item // Create a new Item -> return the Item with an id PUT /v1/todo/list/{listId}/add // add the item defined by {itemId} in the list {listId}
我们可以编写 Action 接口的不同实现:
// src/test/kotlin/com/example/kotlintdd/acceptance/域名 package 域名域名ptance import 域名域名on import 域名域名 import 域名域名List import 域名.HttpEntity import 域名.HttpMethod import 域名.ResponseEntity import 域名.域名Template class RestAction : Action { private val url = "http://localhost:8080/spring-rest/v1" private val restTemplate = RestTemplate() override fun createTodoList(): TodoList { val response: ResponseEntity<TodoList> = restTemplate .exchange("$url/todo", 域名, HttpEntity(TodoList()), TodoList::域名) return 域名!! } override fun createItem(name: String): Item { val response: ResponseEntity<Item> = restTemplate .exchange("$url/item", 域名, HttpEntity(Item(name)), Item::域名) return 域名!! } override fun addItem(todoList: TodoList, item: Item): TodoList { val response: ResponseEntity<TodoList> = restTemplate .exchange( "$url/todo/${域名}/add", 域名, HttpEntity(item), TodoList::域名 ) return 域名!! } }
并且您需要为 GivenWhenThen 的不同实例设置此新操作:
// src/test/kotlin/com/example/kotlintdd/acceptance/域名 package 域名域名ptance import 域名域名on import 域名域名ontext import 域名域名nWhenThen object AcceptanceTest: GivenWhenThen<Action> { override val action: Action = RestAction() } fun <R> given(block: GWTContext<Action, Unit>.() -> R) = 域名n(block)
然后我可以直接复制我的单元测试作为验收测试:
// src/test/kotlin/com/example/kotlintdd/acceptance/域名 package 域名域名ptance import 域名域名 import 域名.域名ngBootTest @SpringBootTest class TodoListAT { @Test fun `I should be able to insert a new item in my todo list`() { given { `a todo list` } and { `an item`("Eat banana") } `when` { `I add the last item into my todo list` } then { `I expect this item is present in my todo list` } } }
结论
我们看到了一个如何使用 Kotlin-TDD 来实现 TDD 技术的具体例子。我们看到,通过编写自定义 DSL,我们提高了测试的可读性,并且即使在几个月后也很容易理解测试。然后我们看到我们可以在不改变我们构建测试的方式的情况下为验收测试重用相同的过程。