Android中的Kotlin DSL,装置和优雅的UI测试

当我遇到Kotlin DSL时,我想:太好了,这对产品开发很可惜,不会派上用场。 但是,我错了:他帮助我们以一种非常简洁而优雅的方式在Android中编写了端到端UI测试。


图片


关于服务,测试数据以及为什么它不是那么简单


首先,简要介绍我们的服务,以便您了解我们为什么做出某些决定。


我们帮助求职者和雇主找到彼此:


  • 雇主注册公司并职位空缺
  • 求职者搜索职位,将其添加到收藏夹,订阅搜索结果,创建简历并发送反馈

为了模拟真实的用户场景并确保应用程序在它们上正确运行,我们需要在服务器上创建所有这些测试数据。 您会说:“因此,预先创建测试雇主和求职者,然后在测试中与他们合作。” 但是有两个问题:


  1. 在测试期间,我们会更改数据;
  2. 测试并行运行。

测试环境和固定装置


端到端测试在测试平台上运行。 他们几乎拥有军事环境,但没有真实数据。 在这方面,当添加新数据时,索引几乎立即发生。


要将数据添加到支架,我们使用特殊的夹具方法。 他们将数据直接添加到数据库并立即对其进行索引:


interface TestFixtureUserApi { @POST("fx/employer/create") fun createEmployerUser(@Body employer: TestEmployer): Call<TestEmployer> } 

夹具仅可从本地网络获得,并且仅适用于测试台。 在开始启动Activity之前立即从测试中调用方法。


DSL


因此,我们做到了多汁。 测试数据如何设置?


 initialisation{ applicant { resume { title = "Resume for similar Vacancy" isOptional = true resumeStatus = ResumeStatus.APPROVED } resume { title = "Some other Resume" } } employer { vacancy { title = "Resume for similar Vacancy" } vacancy { title = "Resume for similar Vacancy" description = "Working hard" } vacancy { title = "Resume for similar Vacancy" description = "Working very hard" } } } 

在初始化块中,我们启动测试所需的实体:在上面的示例中,我们创建了一个有两份履历的求职者,以及一名提供了多个职位空缺的雇主。


为了消除与测试数据的交集相关的错误,我们为测试和每个实体生成唯一的标识符。


实体之间的关系


使用DSL时的主要限制是什么? 由于其树结构,在树的不同分支之间建立连接非常困难。


例如,在我们的申请人申请表中,有一节“适合简历的职位空缺”。 为了使空缺出现在此列表中,我们需要以与当前用户的简历相关的方式进行设置。


 initialisation { applicant { resume { title = "TEST_VACANCY_$uniqueTestId" } } employer { vacancy { title = "TEST_VACANCY_$uniqueTestId" } } } 

为此使用唯一的测试标识符。 因此,在使用该应用程序时,建议为此简历指定指定的职位空缺。 此外,重要的是要注意,此列表中不会出现其他空缺。


初始化相同类型的数据


但是,如果您需要大量空缺怎么办? 每个块都这样复制吗? 当然不是! 我们用一个空缺块来制定一种方法,该方法指示出所需的空缺数,并根据唯一标识符将其多样化。


 initialisation { employer { vacancyBlock { size = 10 transformer = { it.also { vacancyDsl -> vacancyDsl.description = "Some description with text ${vacancyDsl.uniqueVacancyId}" } } } } } 

在vacancyBlock块中,我们指示需要创建多少个空位克隆,以及如何根据序列号转换它们。


处理测试中的数据


在测试期间,处理数据变得非常简单。 我们可以使用我们创建的所有数据。 在我们的实现中,它们存储在用于集合的特殊包装中。 既可以通过作业的订单号(空缺[0]),也可以通过可以在dsl中设置的标签(空缺[“我的空缺”])和快捷方式(vacucies.first())从它们获取数据。


TaggedItemContainer
 class TaggedItemContainer<T>( private val items: MutableList<TaggedItem<T>> ) { operator fun get(index: Int): T { return items[index].data } operator fun get(tag: String): T { return items.first { it.tag == tag }.data } operator fun plusAssign(item: TaggedItem<T>) { items += item } fun forEach(action: (T) -> Unit) { for (item in items) action.invoke(item.data) } fun first(): T { return items[0].data } fun second(): T { return items[1].data } fun third(): T { return items[2].data } fun last(): T { return items[items.size - 1].data } } 

在几乎100%的情况下,编写测试时,我们使用first()和second()方法,其余的则保留灵活性。 以下是在Kakao上进行初始化和测试的测试示例


 initialisation { applicant { resume { title = "TEST_VACANCY_$uniqueTestId" } } }.run { mainScreen { positionField { click() } jobPositionScreen { positionEntry(vacancies.first().title) } searchButton { click() } } } 

DSL不适合什么


所有数据都可以放入DSL吗? 我们的目标是使DSL尽可能简洁明了。 在我们的实施中,由于申请人和雇主的工作顺序并不重要,因此不可能适应他们的关系-回应。
响应的创建已在下一个块中通过对服务器上已创建的实体的操作执行。


DSL实施


从本文中可以了解到,用于指定测试数据和执行测试的算法如下:


  • DSL的一部分在初始化时解析;
  • 根据获得的值,在服务器上创建测试数据。
  • 执行可选的转换块,您可以在其中设置响应。
  • 使用已经确定的数据集执行测试。

解析来自初始化块的数据


那里正在发生什么魔术? 考虑如何构造顶级TestCaseDsl元素:


 @TestCaseDslMarker class TestCaseDsl { val applicants = mutableListOf<ApplicantDsl>() val employers = mutableListOf<EmployerDsl>() val uniqueTestId = CommonUtils.unique fun applicant(block: ApplicantDsl.() -> Unit = {}) { val applicantDsl = ApplicantDsl( uniqueTestId, uniqueApplicantId = CommonUtils.unique applicantDsl.block() applicants += applicantDsl } fun employer(block: EmployerDsl.() -> Unit = {}) { val employerDsl = EmployerDsl( uniqueTestId = uniqueTestId, uniqueEmployerId = CommonUtils.unique employerDsl.block() employers += employerDsl } } 

在申请人方法中,我们创建ApplicantDsl。


申请人Dsl
 @TestCaseDslMarker class ApplicantDsl( val uniqueTestId: String, val uniqueApplicantId: String, var tag: String? = null, var login: String? = null, var password: String? = null, var firstName: String? = null, var middleName: String? = null, var lastName: String? = null, var email: String? = null, var siteId: Int? = null, var areaId: Int? = null, var resumeViewLimit: Int? = null, var isMailingSubscription: Boolean? = null ) { val resumes = mutableListOf<ResumeDsl>() fun resume(block: ResumeDsl.() -> Unit = {}) { val resumeDslBuilder = ResumeDsl( uniqueTestId = uniqueTestId, uniqueApplicantId = uniqueApplicantId, uniqueResumeId = CommonUtils.unique ) resumeDslBuilder.apply(block) this.resumes += resumeDslBuilder } } 

然后,我们从以下块块对其执行操作:ApplicantDsl。()-> Unit。 正是这种设计使我们能够轻松处理DSL中的ApplicantDsl字段。


请注意,在执行该块时,已经设置了uniqueTestId和uniqueApplicantId(用于相互连接实体的唯一标识符),我们可以访问它们。


初始化块在内部具有类似的结构:


 fun initialisation(block: TestCaseDsl.() -> Unit): Initialisation { val testCaseDsl = TestCaseDsl().apply(block) val testCase = TestCaseCreator.create(testCaseDsl) return Initialisation(testCase) } 

我们创建一个测试,对其应用阻止操作,然后使用TestCaseCreator在服务器上创建数据并将其放入集合中。 TestCaseCreator.create()函数非常简单-我们遍历数据并在服务器上创建它。


陷阱和想法


某些测试非常相似,仅在输入数据和控制其显示的方式上有所不同(例如,当空缺中指示不同的货币时)。

在我们的案例中,很少有这样的测试,因此我们决定不使用特殊语法使DSL混乱


在DSL出现之前的几天里,我们为数据建立索引很长时间,为了节省时间,我们在一类中进行了很多测试,并在一个静态块中创建了所有数据。

不要这样做-这将使您无法重新启动失败的测试。 事实是,在失败测试的启动过程中,我们可以更改服务器上的初始数据。 例如,我们可以在您的收藏夹中添加一个空缺。 然后,当您重新开始测试时,单击星号会相反地导致从“收藏夹”列表中删除空缺,这是我们所不期望的行为。


总结


这种指定测试数据的方法大大简化了测试工作:
在编写测试时,您无需考虑是否有服务器以及以什么顺序初始化数据。
可以在服务器上设置的所有实体都可以轻松显示在IDE提示中。
有一种相互初始化和通信数据的方法。


相关资料


如果您对我们的UI测试方法感兴趣,那么在开始之前,建议您熟悉以下材料:



接下来是什么


本文是有关用于编写和支持Android中的UI测试的工具和高级框架的系列文章的第一篇。 随着新零件的推出,我将它们链接到本文。

Source: https://habr.com/ru/post/zh-CN455042/


All Articles