本文是在Kotlin Multiplatform中应用MVI架构模式的系列文章的最后一篇。在前两部分(第1部分和第2部分)中,我们记得MVI是什么,创建了一个通用的Kittens模块来加载猫图像并将其集成到iOS和Android应用程序中。
在这一部分中,我们将通过单元测试和集成测试来介绍Kittens模块。我们将了解Kotlin Multiplatform中当前的测试局限性,弄清楚如何克服它们,甚至使它们对我们有利。
在我们的GitHub上有更新的示例项目。
序幕
毫无疑问,测试是软件开发中的重要一步。当然,这会减慢该过程的速度,但同时:
- 使您可以检查难以手动捕获的边缘情况;
- 减少在添加新功能,修复错误和重构时进行回归的机会;
- 强迫您分解和构造代码。
乍一看,最后一点似乎很不利,因为这需要时间。但是,从长远来看,它使代码更具可读性和益处。
“实际上,读与写所花费的时间比例远远超过10:1。作为编写新代码的一部分,我们不断读取旧代码。... [因此,]使其易于阅读使其易于书写。” -Robert C. Martin,“清洁代码:敏捷软件工艺手册”
Kotlin Multiplatform扩展了测试功能。该技术增加了一个重要功能:每个测试都在所有受支持的平台上自动执行。例如,如果仅支持Android和iOS,则测试次数可以乘以2。并且,如果在某个时候添加了对另一个平台的支持,那么它将自动成为测试的一部分。
在所有支持的平台上进行测试很重要,因为代码的行为可能有所不同。例如,Kotlin / Native具有特殊的内存模型,Kotlin / JS有时也会产生意外的结果。
在继续进行之前,值得一提的是Kotlin Multiplatform中的一些测试限制。最大的问题是缺少Kotlin / Native和Kotlin / JS的任何模拟库。这似乎是一个很大的缺点,但我个人认为这是一个优点。对我来说,在Kotlin Multiplatform中进行测试非常困难:我必须为每个依赖项创建接口并编写其测试实现(伪造)。它花了很长时间,但是在某个时候,我意识到花时间在抽象上是一项投资,可以使代码更简洁。
我还注意到,对该代码的后续修改花费的时间更少。这是为什么?因为没有钉牢(嘲笑)类及其依赖项的交互。在大多数情况下,仅更新其测试实现就足够了。无需深入研究每种测试方法即可更新模拟。结果,即使在标准的Android开发中,我也停止使用模拟库。我建议阅读以下文章:Pravin Sonawane的“模拟不切实际-使用假货” 。
计划
让我们记住小猫模块中的内容以及应该测试的内容。
- KittenStore是模块的主要组件。它的KittenStoreImpl实现包含大多数业务逻辑。这是我们要测试的第一件事。
- KittenComponent是所有内部组件的模块外观和集成点。我们将通过集成测试涵盖该组件。
- KittenView是一个公共接口,代表KittenComponent的UI依赖项。
- KittenDataSource是一个内部Web访问界面,具有针对iOS和Android的特定于平台的实现。
为了更好地理解该模块的结构,我将给出其UML图:
该计划如下:
- 测试KittenStore
- 创建KittenStore.Parser的测试实现
- 创建KittenStore.Network的测试实现
- KittenStoreImpl的编写单元测试
- 创建KittenStore.Parser的测试实现
- 测试KittenComponent
- 创建KittenDataSource的测试实现
- 构建测试KittenView实施
- 为KittenComponent编写集成测试
- 创建KittenDataSource的测试实现
- 运行测试
- 结论
KittenStore单元测试
KittenStore接口具有其自己的实现类-KittenStoreImpl。这就是我们要测试的。它具有两个依赖项(内部接口),直接在类本身中定义。让我们开始为它们编写测试实现。
测试KittenStore.Parser的实现
该组件负责网络请求。其界面如下所示:
在编写网络接口的测试实现之前,我们需要回答一个重要的问题:服务器返回什么数据?答案是服务器每次返回一组随机的图像链接。在现实生活中,使用的是JSON格式,但是由于我们具有Parser抽象,因此我们不在乎单元测试中的格式。
真正的实现可以切换流,因此可以将订阅者冻结在Kotlin / Native中。为这种行为建模可以确保代码正确处理所有事情。
因此,我们的网络测试实现应具有以下功能:
- 必须为每个请求返回一组非空的不同行;
- 网络和解析器的响应格式必须相同;
- 应该能够模拟网络错误(也许应该在没有响应的情况下完成);
- 必须有可能模拟无效的响应格式(以检查解析器中的错误);
- 应该有可能模拟响应延迟(以检查启动阶段);
- 应该在Kotlin / Native中可冻结(以防万一)。
测试实现本身可能如下所示:
TestKittenStoreNetwork有一个字符串存储(就像真实的服务器一样)并且可以生成它们。对于每个请求,当前行列表被编码为一行。如果“ images”属性为零,则Maybe可能刚刚终止,应该认为是错误。
我们还使用了TestScheduler。该调度程序具有一项重要功能:它冻结所有传入的任务。因此,与TestScheduler结合使用的observeOn运算符将冻结下游以及流经下游的所有数据,就像在现实生活中一样。但是同时,不会涉及多线程,这简化了测试并使其更加可靠。
此外,TestScheduler具有特殊的“手动处理”模式,该模式将允许我们模拟网络延迟。
测试KittenStore.Parser的实现
该组件负责解析服务器的响应。这是它的界面:
因此,从网上下载的所有内容都应转换为链接列表。我们的网络只是使用分号(;)分隔符来连接字符串,因此在此处使用相同的格式。
这是一个测试实现:
与网络一样,TestScheduler用于冻结订户并检查其与Kotlin /本机内存模型的兼容性。如果输入字符串为空,则会模拟响应处理错误。
KittenStoreImpl的单元测试
现在,我们具有所有依赖项的测试实现。现在该进行单元测试了。所有单元测试都可以在存储库中找到,这里我仅给出初始化和一些测试本身。
第一步是创建我们的测试实现的实例:
KittenStoreImpl使用mainScheduler,因此下一步是覆盖它:
现在我们可以运行一些测试。KittenStoreImpl应该在创建后立即加载图像。这意味着必须满足网络请求,必须处理其响应,并且必须用新结果更新状态。
我们做了什么:
- 在网络上生成图像;
- 创建了一个KittenStoreImpl的新实例;
- 确保状态包含正确的字符串列表
我们需要考虑的另一种情况是获取KittenStore.Intent.Reload。在这种情况下,必须从网络重新加载列表。
测试步骤:
- 生成源图像;
- 创建KittenStoreImpl的实例;
- 生成新图像;
- 发送Intent.Reload;
- 确保条件包含新图像。
最后,让我们检查以下情况:在加载图像时设置isLoading标志。
我们为TestScheduler启用了手动处理-现在将不会自动处理任务。这使我们可以在等待响应时检查状态。
KittenComponent集成测试
如前所述,KittenComponent是整个模块的集成点。我们可以进行集成测试。让我们看一下它的API:
有两个依赖项,即KittenDataSource和KittenView。在开始测试之前,我们将需要测试实现。
为了完整起见,此图显示了模块内部的数据流:
测试KittenDataSource的实现
该组件负责网络请求。每个平台都有单独的实现,我们需要针对测试的另一个实现。这是KittenDataSource接口的样子:
TheCatAPI支持分页,因此我立即添加了适当的参数。否则,它与我们之前实现的KittenStore.Network非常相似。唯一的区别是,我们在集成中测试真实代码时必须使用JSON格式。因此,我们只是借鉴了实现思路:
和以前一样,我们会在每个请求中生成不同的字符串列表,这些字符串被编码为JSON数组。如果没有图像生成,或者请求参数错误,也许会终止而没有响应。kotlinx.serialization
库用于形成JSON数组。顺便说一下,经过测试的KittenStoreParser使用它进行解码。
测试KittenView的实现
这是我们开始测试之前需要测试实现的最后一个组件。这是它的界面:
这个视图只接受模型并触发事件,因此其测试实现非常简单:
我们只需要记住最后接受的模型-这将使我们能够检查所显示模型的正确性。我们还可以使用在继承的AbstractMviView类中声明的dispatch(Event)方法代表KittenView调度事件。
KittenComponent的集成测试
完整的测试集可以在存储库中找到,在这里我仅给出一些最有趣的测试。
和以前一样,让我们首先实例化依赖关系并进行初始化:
当前,该模块有两个调度程序:mainScheduler和CalculationScheduler。我们需要覆盖它们:
我们现在可以编写一些测试。让我们首先检查主脚本,以确保在启动时加载并显示了图像:
该测试与我们查看KittenStore的单元测试时编写的测试非常相似。直到现在才涉及整个模块。
测试步骤:
- 在TestKittenDataSource中生成图像的链接;
- 创建并运行KittenComponent;
- 确保链接到达TestKittenView。
另一个有趣的情况:KittenView触发RefreshTriggered事件时需要重新加载图像。
阶段:
- 生成图像的源链接;
- 创建并运行KittenComponent;
- 产生新的链接;
- 代表KittenView发送Event.RefreshTriggered;
- 确保新链接到达TestKittenView。
运行测试
要运行所有测试,我们需要执行以下Gradle任务:
./gradlew :shared:kittens:build
这将编译模块并在所有支持的平台上运行所有测试:Android和iosx64。
这是JaCoCo的覆盖率报告:
结论
在本文中,我们用单元测试和集成测试介绍了Kittens模块。提议的模块设计使我们能够涵盖以下部分:
- KittenStoreImpl-包含大多数业务逻辑;
- KittenStoreNetwork-负责高层网络请求;
- KittenStoreParser-负责解析网络响应;
- 所有的转换和联系。
最后一点很重要。借助MVI功能,可以覆盖它。该视图的唯一责任是显示数据和调度事件。所有订阅,转换和链接都在模块内部完成。因此,我们可以用常规测试覆盖除显示屏本身以外的所有内容。
这样的测试具有以下优点:
- 不使用平台API;
- 表演很快
- 可靠(不闪烁);
- 在所有支持的平台上运行。
我们还能够测试该代码与复杂的Kotlin /本机内存模型的兼容性。这也是非常重要的,因为在构建时缺乏安全性:代码仅在运行时崩溃,并带有难以调试的异常。
希望这对您的项目有所帮助。感谢您阅读我的文章!并且不要忘记在Twitter上关注我。
...
奖金运动
如果您想使用测试实现或使用MVI,这里有一些动手练习。
重构KittenDataSource
该模块中有KittenDataSource接口的两种实现:一种用于Android,一种用于iOS。我已经提到他们负责网络访问。但是它们实际上还有另一个功能:它们根据输入参数“ limit”和“ page”生成请求的URL。同时,我们有一个KittenStoreNetwork类,除了将调用委派给KittenDataSource外,它什么也不做。
分配:将URL请求生成逻辑从KittenDataSourceImpl(在Android和iOS上)移动到KittenStoreNetwork。您需要按以下方式更改KittenDataSource接口:
完成后,将需要更新测试。您需要触摸的唯一类是TestKittenDataSource。
添加页面加载
TheCatAPI支持分页,因此我们可以添加此功能以获得更好的用户体验。您可以通过为KittenView添加一个新的Event.EndReached事件开始,此后代码将停止编译。然后,您将需要添加适当的Intent.LoadMore,将新的Event转换为Intent,然后在KittenStoreImpl中对其进行处理。您还需要修改KittenStoreImpl.Network接口,如下所示:
最后,您将需要更新一些测试实现,修复一个或两个现有测试,然后编写一些新的测试来覆盖分页。