写作测试是程序员的关键技能。在这篇文章中,我们将走过所需的一切,以便成为熟练的测试作家。无论您是刚开始写作测试,还是希望加深您对主题的知识,我们将在这里为您提供一些东西。

虽然以下例子在Kotlin,但这些概念适用于整个行业。片段没有任何第三方图书馆,我将根据需要解释独特的kotlin关键字。您将能够接受您在此学习的内容并将其应用于您最爱的平台和语言。

在我们踏上我们的冒险之前,让我们了解我们为什么这样做。

为什么我们写测试

单元测试是一种工具,允许我们编写验证应用程序代码行为的代码。起初它看起来像是刚刚进入圈子,但是有很多很大的好处:

  1. 写作测试可以节省我们的时间从通过应用程序所需的手动努力。
  2. 我们的测试运行可以自动化,因此我们可以切换到不同的任务,而某些外部过程验证我们的代码。
  3. 测试允许我们将我们的代码运送得多,因为我们已经确定了我们想要验证和验证的方案。
  4. 简单地写作测试的行为可以帮助您编写更可靠的代码。随着时间的推移,您将习惯于考虑重要的限制和流量,这些限制性和流量都很重要,并且在写入测试时,您将在开发过程中而不是追溯思考它们。

现在我们相信,让我们来看看我们想要测试的课程。

样本类

在此示例中,我们要测试获取和显示某些数据的ViewModel类。它应该有支持显示数据列表,无数据和全部,或获取数据错误。我们将使用我最喜欢的榜样,是口袋妖怪列表。

class pokemonlistviewmodel {private var state:pokemonliststate?= null val pokemon:list ?get()=(陈述?pokemonliststate.loaded)?data val sprolling:boolean get()= state是pokemonliststate.loading val showerror:boolean get()= state是pokemonliststate.error val showdata:boolean get()= state是pokemonliststate.loaded val showemptystate:boolean get()= state是pokemonliststate.empty init {fetchpokemomonlist()}私人有趣fetchpokemomonlist(){state = pokemonliststate.load trive try {val pokemonlist = pokemonservice()。getPokemon()状态=如果(pokemomonlist.isuspley()){pokemomonliststate.preasty} else {pokemonliststate.loaded(pokemonlist)}} catch(错误:throwable){state = pokemonliststate.error(错误)}}}

除此文件外,让我们看看另外两个相关类,只是为了了解它们的样子:

/ ** * pokemonservice是负责请求我们所有网络数据的课程。* / class pokemonservice {fun getPokemon():列表 {//从网络获取数据}} / ** * pokemonliststate是每个可能的屏幕可以进入的密封类。*如果你是kotlin的新手,想想这些就像一个枚举类型。* / Sealed类PokemonListstate {对象加载:pokemonliststate()对象空

识别测试用例

在我们编写测试之前,我们应该准确了解我们想要测试的内容。保持那些测试用例的思想对于确保我们编写支持测试的好代码非常重要,这是我们谈论下一个。

测试一种方法是查看每一个公共方法和属性,并为每个公共方法和属性写一次测试用例以验证其行为。它还可能有助于在相关的用户体验方面考虑测试案例,这是我们在这里要做的事情。我们的ViewModel有三种可能的场景:

  1. 当页面加载时,用户会看到口袋妖怪列表。
  2. 当页面加载时,用户会看到空状态,因为它们没有口袋妖怪。
  3. 当页面加载时,用户会看到错误消息,因为应用程序无法请求信息。

确定了我们想要测试的内容,让我们确保我们甚至可以这样做。

写可测试的代码

令人惊讶的是,并非所有代码都可以是测试的。我给你的例子是一个无法准确测试的例子。

避免硬编码依赖性

在尝试测试我们的视图时我们将拥有的问题之一:

val pokemonlist = pokemonservice()。getPokemon()

在测试用例中,我们已识别,我们希望验证用于检索数据的三种不同方案。但是,由于我们的观点是直接依赖pokemonservice.,我们是怜悯那个班级的行为。我们无法告诉它成功返回或错误。

问题是我们内心的依赖PokemonListViewModel.我们无法在测试期间控制。我们可以使用构造函数将服务传递给ViewModel类来解决这个问题。这通常被称为“依赖注入”。

Class PokemomonListViewModel(Private Val服务:Pokemonservice){// ...私人有趣fetchpokemomonlist(){state = pokemonliststate.load trive {val pokemonlist = service.getpokemon()// ...} catch(错误:throwable){状态= pokemonliststate.error(错误)}}}}}

使用接口定义预期的行为

虽然我们已经解决了具有内在依赖的问题,但我们有一个新的问题pokemonservice.范围。让我们重新审视课程:

class pokemonservice {fun getPokemon():列表 {//从网络获取数据}}}

在此类中,我们有一个已经定义的从网络获取数据的方式。这意味着我们的测试代码我无法修改它以伪造特定响应。支持此选项是创建一个接口。接口是定义预期行为的方法,并将实现详细信息留给类。

通过创建界面,我们可以在我们的应用程序代码中有一个实现来与我们的网络交谈,并为我们的测试实现返回虚假响应的一个实现。

一旦我们创建了该界面,我们现在可以将该类型传递到我们的视图中:

接口PokemonRepository {Fun GetPokemon():list }类PokemonListViewModel(Private Val存储库:PokemonRepository){// ...}

嘲笑依赖性

在我们编写测试之前,我们需要做的最后一件事就是模拟这些数据请求。为我们的测试创建孤立的环境非常重要,以便运行。虽然实际应用程序连接到Internet,但我们不想依赖于设备测试。我们也不希望将网站带下来只是为了验证我们的错误方案。;)

现在,我们的课程是没有依赖的,并且它需要的行为由接口备份,我们可以为我们定义的每个测试用例创建一个实现:

//返回一个真正的pokemon类成功列表:pokemomonRepository {override fun getPokemon():list  {return listof(pokemon(name =“squirtle”)))} //返回一个空名单的口袋妖怪类emptyrepository:pokemonrepository{override fun getPokemon():列表 {return emptylist()}} //抛出一个例外以模拟网络错误类errorpository:pokemonRepository {override fun getPokemon():list  {drown throwable(“oblow throwable(”owlops))}}}

写我们的测试

我们采取了课堂并重新开始推荐它以支持测试。我们已经创建了必要的嘲笑数据。现在我们可以编写我们的第一个测试。让我们首先走过我们的成功案例:

  1. 使用我们创建视图模型成功的延期
  2. 断言ViewModel公开的口袋妖怪列表匹配我们的假存储库所期望的。
  3. 验证showdata.是真的,而且淋浴道showloving.萨默塞特都是假的。

这是它看起来的。如果你是新的测试,assertequals.是一种采用两个参数的方法。第一个是你期待的价值,第二个是你正在检查的价值。如果这些不等于,它将抛出异常并使您的测试失败。

class pokemonlistviewmodeltest {@test fun成功= listof(pokemon(name =“squirtle”))val视图= pokemonlistviewmodel(appecticrepository())assertequals(truepokemon.pokemon)assertequals(true,ViewModel.showdata)assertequals(false,ViewModel.showloading)Assertequals(false,ViewModel.Showerror)Assertequals(False,ViewModel.showemptyState)}}}

负测试案例

写入单元测试时,也很重要的是考虑错误方案。崩溃是令人沮丧的用户体验。当某些错误发生时,确保我们不会崩溃更好,然后显示相关的错误消息。编写模拟这些流的测试单元测试确保避免了优雅地处理错误,并避免出乎意料的崩溃。

跟随我们已经了解到这一点的一切,我们可以从第一次测试中获取代码并为其他案例调整它。我们需要做的就是更改存储库,并修改相应的存储库assertequals.称呼。

class pokemonlistviewmodeltest {@test fun extentfetch(){valipemodel = pokemonlistviewmodel(viplyRepository())assertequals(false,ViewModel.showdata)assertequals(false,ViewModel.showloading)Assertequals(false,ViewModel.Showerror)assertequals(true,tevelmodel.showemptystate)} @test fun errorfetch(){valipemodel = pokemonlistviewmodel(errorrepository())assertequals(null,ViewModel.Pokemon)assertequals(false,ViewModel.showdata)assertequals(false,ViewModel.showloading)assertequals(true,tevemodel.showerror)assertequals(false,wearemodel.showemptystate)}}

测试需要独立

请注意,在上面的示例中,每个测试都会创建自己的视图模型。这实际上是故意的。在编写测试时,我们应该确保每个测试彼此独立运行。这是因为类的所有测试都运行在一起,如果有任何属性遍历多个测试,我们可能会产生意外的后果。

通过确保每个测试都有自己的ViewModel,我们可以相信没有外部因素可能会导致失败。

这些测试的问题

让我们花点时间看看到目前为止我们已经完成了什么。我们拍摄了一些我们想要测试的代码,将其重组为分开问题并使您更容易测试,学会了如何模拟数据请求,然后验证了该行为。只有这三个测试是从我们所在的地方一大一步,我们可以随着我们之前提到的所有其他利益而继续发货。

当我们继续为我们的代码库编写更多测试时,我们将达到几个障碍。

  1. 那些重复的东西assertequals.呼叫真的粗糙在眼睛上。当你滚动测试时,他们会有点流血,特别是因为他们在测试中看起来也一样。
  2. 每个测试紧密耦合到视图。我的意思是,每个测试都直接访问视图上的字段,如果我们要改变一些事情ViewModel.Pokemon.例如,我们必须更新引用它的每个测试。这最终可能会耗时。

我们拥有的一个解决方案是机器人模式。

测试机器人

回想起我们在删除依赖的开始时解决的问题pokemonservice.。除了测试目的之外,这可能对我们的视图典礼造成了问题,因为它需要对PokeMonservice的特定知识,以及我们的观点典礼。通过隐藏界面背后的行为,我们创建了一个系统,可以在其中改变实施细节pokemonservice.不必用它更新我们的视图。

同样,我们可以为我们的测试创建一个系统,以这种方式也是如此。如果我们考虑我们的测试试图验证的东西,他们真的只是说“这是预期的口袋妖怪列表。”测试不需要知道该列表如何暴露的具体细节。

如果我们的测试代码如下阅读,那么它不会很好吗?

@Test乐趣successfulFetch(){VAL expectedPokemon = listOf(口袋妖怪(NAME = “小水龟”))PokemonListViewModelRobot().buildViewModel(库= SuccessfulRepository()).assertPokemonList(expectedPokemon).assertShowData(真).assertShowError(假).assertShowLoading(false).assertshowemptystate(false)}

创建测试机器人

要隐藏所有ViewModel从测试类中的实现细节,我们需要创建另一个类(我们的机器人类),我们将所有的行为包装。这意味着我们的机器人需要包含要测试的组件的引用,PokemonListViewModel.

第一步是让我们的机器人定义它仿真的组件,在这种情况下,viewModel,并公开一种创建该组件的方法。

class pokemonlistviewmodelrobot {private lateinit var Viewmodel:pokemonlistviewmodel fun buildviewmodel(存储库:pokemonrepository):pokemonlistviewmodelrobot {this.viewmodel = pokemonlistviewmodel(存储库)返回此}}

在这个片段,胶林机是一个kotlin关键字,只是说我们定义了这个属性,但我们稍后会给它一个值,我们在里面做了一个值BuildViewModel.

另请注意,我们的方法的最后一行返回机器人的实例。允许我们做的事情是反复连锁调用,就像我们在最后一节中所做的那样。

断言任何公共属性或getter方法

我们之前指出,我们的测试不需要知道属性的实施。所有它需要做的就是主张价值。因此,对于我们的班级可用阅读的任何信息,我们可以创建相应的assettthing()在我们的机器人中:

class pokemomllistviewmodelrobot {fun sermememon:list ?):pokemomonlistviewmodelrobot {val amantypokemon = ViewModel.Pokemon Assertequals返回此}有趣的assertshowloading(预期的show:boolean):pokemonlistviewmodelrobot {val ameralshowing = ViewModel.showloading assequals(预期的播出,实际播出)返回此} // ...}

代理任何公共方法调用

在我们的示例中,我们没有任何我们可以在ViewModel上呼叫的公共方法。但是,如果我们这样做,机器人也将负责仅将信息传递给这些电话。例如,让我们说我们的视图表达了一个公共方法loadmorepokemon(count:int)。我们的机器人可以通过这种方式实现:

class pokemonlistviewmodelrobot {fun loadmorepokemon(counttoload:int):pokemonlistviewmodelrobot {ViewModel.LoadMorePokemon(CountToload)返回此}}

完成示例

现在我们的机器人已经涵盖了我们想要测试的每一个公共方法和财产,我们都可以重构我们所有的测试:

@Test乐趣successfulFetch(){VAL expectedPokemon = listOf(口袋妖怪(NAME = “小水龟”))PokemonListViewModelRobot().buildViewModel(库= SuccessfulRepository()).assertPokemonList(expectedPokemon).assertShowData(真).assertShowError(假).assertShowLoading(false)} @test fun emptyfetch(){pokemonlistviewmodelrobot().buildviewmodel(repository = emptyrepository()).assertpokemonlist(null).assertshowemptystate(true).assertshowerror(false).assertshowloading(false).assertshowdata(false).assertshowdata(false).assertshowdata(false)} @测试有趣errorfetch(){pokemonlistviewmodelrobot().buildviewmodel(repository = errorrupository()).assertpokemonlist(null).assertshowerror(true).assertshowloading(false).assertshowdata(false)}

机器人行动

这些新测试的可读性以及通过链接调用轻松创建测试的能力可能是足以采用这种模式的原因。要查看另一个实例,测试机器人可以发光的力量,让我们更改我们的视图,并查看更新我们的测试是多快的。

记得我们的showdata.财产?这是我们测试中的每一个都引用的布尔属性。让我们将其更改为整数,例如sake:

class pokemomonlistviewmodel {// ... val showdata:int get(){if(state是pokemonliststate.loaded){return 1} else {return 0}}}

现在我们所有的测试都失败了,因为他们期待真的代替1。想象一下,如果我们有几十个测试,而不是三个。

谢天谢地,我们重构了我们所有的测试,以使用机器人类,我可以在机器人内处理此调整。

class pokemomlistviewmodelrobot {fun sharertshowdata(pusinshows:boolean):pokemonlistviewmodelrobot {//将此布尔映射到viewmodel val的int暴露的int indectint = if(permanialshowing)1 else0 0 val ameralshowing = ViewModel.showdata Assertequals(ChecipantInt,ActualShowing)返回此}}}

随着两个线的变化,我们所有的测试都会再次传递。

搭档

哇!一个冗长的帖子,但你做到了。让我们简要回顾我们学到的一切:

  • 在我们可以为我们的代码编写单元测试之前,我们必须确保代码甚至可测试。
    • 我们通过避免任何内部依赖项来执行此操作,而不是将它们传递给。
    • 我们可以在可能的情况下定义使用接口的行为,以便我们可以控制在测试用例中的实现。
    • 如果我们首先定义我们的测试用例,它可以帮助我们确定我们如何构建我们的代码以支持它们。
  • 嘲笑我们的依赖关系。这样做可确保我们的测试以可预测的方式运行,我们不依赖外部因素。
  • 利用测试机器人。这使得我们的测试易于阅读,并允许我们在应用程序代码更改时快速更新我们的测试。

如果您对测试有任何其他疑问,请联系我推特

有兴趣在一个小团队上追求这样的伟大新做法吗?我们正在招聘!

资源

要从此博客中查看所有已完成的代码,我将每个文件放入一个GIST:https://gist.github.com/AdammC331/1362FEB28241EBFAC320C21F6E24F341