写测试是程序员的关键技能。在这篇文章中我们将通过你需要知道成为一个熟练编写测试的人都走。无论你是刚开始写测试,或希望加深这方面的知识,我们有东西给你在这里。

虽然下面的例子是在科特林,这些概念是适用于整个行业。该片段是没有任何第三方库的,我会解释独特科特林关键字是必要的。您可以带你在这里学到什么,它适用于平台和语言,你最喜欢的。

我们开始我们的冒险之前,让我们明白我们为什么要这么做。

为什么我们编写测试

单元测试是一个工具,可以让我们说验证了我们的应用程序代码的行为编写代码。起初,它可能看起来像你只是兜兜,但有一些很大的好处:

  1. 编写测试节省了我们从通过应用程序运行所需要的手工劳动时间。
  2. 我们的测试运行可以自动化,所以尽管有些外部进程验证了我们的代码,我们可以切换到不同的任务。
  3. 有试验使我们能够推出我们的代码有更多的信心,因为我们已经确定,我们要验证方案,并验证他们了。
  4. 简单地编写测试的动作可以帮助你编写更可靠的代码。随着时间的推移,你会习惯的各种限制,并是重要流量思考,你最终会编写测试时,在开发过程中思考这些问题,而不是事后。

现在,我们深信,让我们看看在课堂上,我们要测试。

样例类

在这个例子中,我们要测试一个视图模型类,取出并显示一些数据。它应该有显示数据,没有数据和全部或错误获取数据的列表支持。我们会用我最喜欢的例子,宠物小精灵列表。

类PokemonListViewModel {VAR私有状态:PokemonListState?= NULL VAL小宠物:列表<​​口袋妖怪>?得到()=(状态PokemonListState.Loaded?)?.数据VAL showLoading:布尔的get()=状态是PokemonListState.Loading VAL showError:布尔的get()=状态是PokemonListState.Error VAL showData:布尔的get()=状态是PokemonListState.Loaded VAL showEmptyState:布尔的get()=状态PokemonListState.Empty的init {fetchPokemonList()}私人乐趣fetchPokemonList(){状态= PokemonListState.Loading尝试{VAL pokemonList = PokemonService()getPokemon()状态= IF(pokemonList.isEmpty()){} PokemonListState.Empty否则{PokemonListState.Loaded(pokemonList)}}赶上(错误:Throwable的){状态= PokemonListState.Error(误差)}}}

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

/ ** * PokemonService负责,要求所有网络数据为我们的类。* /类PokemonService {乐趣getPokemon():名单<神奇宝贝> {//获取来自网络的数据}} / ** * PokemonListState是一个密封类每个可能的状态我们的屏幕可以在*如果你是新来科特林,想到这些像一个枚举类型。* /密封类PokemonListState {对象装载:PokemonListState()对象空:PokemonListState()类加载(VAL数据:列表<口袋妖怪>):PokemonListState()类错误(VAL错误:Throwable的):PokemonListState()}

确定测试用例

之前,我们可以写我们的测试中,我们应该明白正是我们想要测试。保持这些测试用例考虑是确保我们正在编写好的代码,支持测试,这是我们谈论未来非常重要的。

一种方法来测试是看每一个公共方法和属性,并编写测试用例为他们每个人来验证他们的行为。它也可能有助于相对于相关的用户体验,这就是我们要在这里做思考的测试用例。还有我们的视图模型以上三种可能的情况:

  1. 页面加载时,用户看到的小宠物列表。
  2. 页面加载时,用户会看到一个空的状态,因为他们没有宠物小精灵。
  3. 页面加载时,用户会看到一个错误信息,因为应用程序无法请求信息。

在确定我们想要测试,让我们确保我们甚至可以做到这一点。

编写测试代码

出人意料的是,不是所有的代码可以被单元测试。我给你上面的例子是一个无法准确测试。

避免硬编码的依赖

一个试图与这条线就在这里测试我们的视图模型谎言,当我们将有问题:

VAL pokemonList = PokemonService()。getPokemon()

在我们确定的测试案例中,我们要验证三种不同的方案用于检索数据。但是,由于我们的视图模型有直接依赖关系PokemonService,我们在该类行为的摆布。我们没有告诉它成功或返回一个错误的方式。

问题是,我们有一个内部的依赖PokemonListViewModel我们不能在我们的测试控制。我们可以通过构造函数传递服务的视图模型类解决这个问题。这通常被称为“依赖注入”。

类PokemonListViewModel(私人VAL服务:PokemonService){// ...私人乐趣fetchPokemonList(){状态= PokemonListState.Loading尝试{VAL pokemonList = service.getPokemon()// ...}赶上(错误:Throwable的){状态= PokemonListState.Error(误差)}}}

定义预期的行为有了接口

虽然我们已经解决了具有内部依赖性的问题,我们有一个新的问题,我们的PokemonService参数。我们再看一下类:

类PokemonService {乐趣getPokemon():列表<口袋妖怪> {//获取从网络数据}}

这个类里面我们有一个已定义的从网络获取数据的方式。从我们的测试代码,这意味着我不能修改这个伪造具体的回应。支持此一种选择是创建一个接口。接口是一种方法来定义预期的行为,并保留实施细则类。

通过创建一个接口,我们可以有我们的应用程序代码中的一个实现谈谈我们的网络,并为我们的测试一个实现,它返回假响应。

一旦我们已经创建了一个接口,我们可以通过这种类型的进入我们的视图模型现在:

接口PokemonRepository {乐趣getPokemon():名单<神奇宝贝>}类PokemonListViewModel(私人VAL库:PokemonRepository){// ...}

嘲讽的依赖

我们需要做之前,我们可以写我们的测试的最后一件事是在嘲弄那些数据的请求。这是非常重要的创造我们的测试中运行一个孤立的环境。虽然实际应用程序连接到互联网,我们不希望靠这个了单元测试。我们也不希望把该网站仅下跌来验证我们的错误情况。;)

现在,我们班是免费的依赖,它需要通过一个接口支持的行为,我们可以为每一个我们所定义的测试用例创建一个实现:

//返回口袋妖怪类SuccessfulRepository的真正名单:PokemonRepository {覆盖乐趣getPokemon():名单<神奇宝贝> {回报listOf(口袋妖怪(NAME = “小水龟”))}} //返回口袋妖怪类EmptyRepository的空列表:PokemonRepository{覆盖乐趣getPokemon():列表<口袋妖怪> {返回的emptyList()}} //抛出异常来模拟网络错误类ErrorRepository:PokemonRepository {覆盖乐趣getPokemon():列表<口袋妖怪> {抛出的Throwable( “糟糕”)}}

写我们的测试

我们已经采取了我们班,并重构它支持测试。我们已经建立了必要的嘲笑数据。现在,我们可以写我们的第一个测试。让我们通过我们的成功案例开始步行:

  1. 通过创建一个视图模型我们SuccessfulRepository
  2. 断言口袋妖怪由视图模型暴露的匹配列表我们从我们的假货仓库期待。
  3. 验证showData是真的,showErrorshowLoadingshowEmptyState全是假的。

这里看起来像什么。如果你是新来的测试,的assertEquals是发生在两个参数的方法。第一个是你期待的价值,第二个是你检查对值。如果这些是不相等的,它会抛出一个异常和故障测试。

类PokemonListViewModelTest {@Test乐趣successfulFetch(){VAL expectedPokemon = listOf(口袋妖怪(NAME = “小水龟”))VAL视图模型= PokemonListViewModel(SuccessfulRepository())的assertEquals(expectedPokemon,viewModel.pokemon)的assertEquals(真,viewModel.showData)的assertEquals(假的,viewModel.showLoading)的assertEquals(假,viewModel.showError)的assertEquals(假,viewModel.showEmptyState)}}

负面测试用例

当编写单元测试时,必须考虑误差的情况也很重要。崩溃令人沮丧的用户体验。当发生某些错误,这是更好的,以确保我们没有崩溃,而是显示相应的错误信息。编写单元测试模拟这些流动确保错误被​​正常处理和避免意外崩溃。

继一切,我们已经学会了这一点,我们可以从第一个测试采取的代码,并调整它的其他案件。所有我们需要做的是改变了资源库,并修改相应的的assertEquals呼叫。

类PokemonListViewModelTest {@Test乐趣emptyFetch(){VAL视图模型= PokemonListViewModel(EmptyRepository())的assertEquals(NULL,viewModel.pokemon)的assertEquals(假,viewModel.showData)的assertEquals(假,viewModel.showLoading)的assertEquals(假的,viewModel.showError)的assertEquals(真,viewModel.showEmptyState)} @Test乐趣errorFetch(){VAL视图模型= PokemonListViewModel(ErrorRepository())的assertEquals(NULL,viewModel.pokemon)的assertEquals(假,viewModel.showData)的assertEquals(假的,viewModel.showLoading)的assertEquals(真,viewModel.showError)的assertEquals(假,viewModel.showEmptyState)}}

测试需要独立

请注意,在上面的例子中,每个测试创建自己的视图模型。这实际上是有意这样做的。在编写测试时,我们应该确保每个测试相互独立运行。这是因为所有的类运行测试的在一起,如果有多个测试扛过任何属性,我们可以有意想不到的后果。

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

该问题这些测试

让我们花点时间来看看我们已经取得的成就。我们采取了一些代码,我们想测试,重组它分离关注,并使其更容易测试,学会了如何模拟数据请求,然后验证的行为。刚刚这三个测试是从那里我们迈出了一大步,我们可以去与更多的信心,并与所有我们前面提到的其他好处出货。

随着我们继续前进,为我们的代码库,虽然写更多的测试,我们会打几个路障。

  1. 这些重复的assertEquals调用是对眼睛真的很粗糙。当你通过测试,滚动,他们会有点流血刚一起,特别是因为他们看他们整个测试相同。
  2. 每个测试被紧密耦合到所述视图模型。我的意思是每个测试直接访问的视图模型领域,如果我们要改变一些事情viewModel.pokemon例如,我们将不得不为每一位测试更新引用它。这可能最终会被真正费时。

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

测试机器人

回想一下,我们删除依赖于我们在开始解决的问题PokemonService。除了测试的目的,这可能会造成问题,为我们的视图模型,因为它需要PokemonService的具体知识,如果它改变,从而将我们的视图模型。通过隐藏接口背后的行为,我们创建了一个系统,我们可以改变的实施细则PokemonService而不必用它来更新我们的视图模型。

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

那岂不是很好,如果我们的测试代码这样写的?

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

创建一个测试机器人

要隐藏所有的从测试类中的视图模型的实现细节,我们需要创建另一个类(我们的机器人类),我们所有的包装与行为。这意味着我们的机器人需要包含的成分,我们正在测试的参考,PokemonListViewModel

第一步是让我们的机器人定义它的模拟组件,在这种情况下,视图模型,并揭露创建组件的方法。

类PokemonListViewModelRobot {私人lateinit VAR视图模型:PokemonListViewModel乐趣buildViewModel(库:PokemonRepository):PokemonListViewModelRobot {this.viewModel = PokemonListViewModel(库)返回此}}

在这个片段中,lateinit是科特林关键字,只是说我们定义的属性,但我们打算以后给它一个价值,这是我们内部做buildViewModel

另外请注意,我们的方法的最后一行返回机器人的实例。是什么,使我们能够做的就是反复调用链在一起,就像我们在最后一节那样。

主张任何公共属性或getter方法

早些时候,我们注意到,我们的测试并不需要知道一个属性的实现。它所需要做的是断言值。因此,对于我们的类可以使用阅读的任何信息,我们可以创建一个相应的assertThing()在我们的机器人:

类PokemonListViewModelRobot {乐趣assertPokemonList(expectedPokemon:列表<口袋妖怪>?):PokemonListViewModelRobot {VAL actualPokemon = viewModel.pokemon的assertEquals(expectedPokemon,actualPokemon)返回此}乐趣assertShowLoading(expectedShowing:布尔型):PokemonListViewModelRobot {VAL actualShowing = viewModel.showLoading的assertEquals(expectedShowing,actualShowing)返回此} // ...}

代理任何公开的方法调用

在我们的例子中,我们没有任何公开的方法,我们可以在视图模型调用。如果我们这样做,不过,机器人也将是只负责通过将信息传递给这些调用。例如,假设我们的视图模型呼吁的公共方法loadMorePokemon(计数:智力)。我们的机器人将实现这种方式:

类PokemonListViewModelRobot {乐趣loadMorePokemon(countToLoad:智力):PokemonListViewModelRobot {viewModel.loadMorePokemon(countToLoad)返回此}}

完成的示例

现在,我们的机器人已经覆盖了我们想测试每一个公共方法和属性,我们可以重构我们所有的测试到这一点:

@Test乐趣successfulFetch(){VAL expectedPokemon = listOf(口袋妖怪(NAME = “小水龟”))PokemonListViewModelRobot().buildViewModel(库= SuccessfulRepository()).assertPokemonList(expectedPokemon).assertShowData(真).assertShowError(假).assertShowLoading(假)} @Test乐趣emptyFetch(){PokemonListViewModelRobot().buildViewModel(库= EmptyRepository()).assertPokemonList(空).assertShowEmptyState(真).assertShowError(假).assertShowLoading(假).assertShowData(假)} @测试乐趣errorFetch(){PokemonListViewModelRobot().buildViewModel(库= ErrorRepository()).assertPokemonList(空).assertShowError(真).assertShowLoading(假).assertShowData(假)}

机器人在行动

这些新测试的可读性和轻松地通过链接调用起来可能有足够的理由采取这种模式创建测试的能力。看到另一个例子测试机器人的力量可以大放异彩,让我们改变了我们的视图模型,看看它是如何迅速地更新我们的测试。

还记得我们showData属性?这是通过我们的测试中的每一个引用的布尔属性。让我们把它变成一个整数,只是举例缘故:

类PokemonListViewModel {// ... VAL showData:诠释的get(){如果(状态PokemonListState.Loaded){返回1}其他{返回0}}}

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

值得庆幸的是我们重构了我们所有的测试使用机器人类的,我可以只处理机器人里面这个调整。

类PokemonListViewModelRobot {乐趣assertShowData(expectedShowing:布尔型):PokemonListViewModelRobot {//地图这个布尔由视图模型VAL expectedInt =如果(expectedShowing)1否则为0 VAL actualShowing = viewModel.showData的assertEquals暴露的int(expectedInt,actualShowing)返回此}}

有了这样两个线路的变化,我们所有的测试都再次通过。

概括

哇!一个漫长的职位,但你做到了。让我们简要概括一切,我们已经了解到:

  • 之前,我们可以写我们的代码单元测试,我们必须确保代码是即使测试。
    • 我们以避免任何内部依赖关系,而是通过他们做到这一点。
    • 我们可以在使用的接口,使我们能够控制我们的执行测试用例定义行为。
    • 如果我们首先定义我们的测试用例,它可以帮助我们确定我们如何构建我们的代码来支持他们。
  • 嘲笑我们的依赖。这样做可以确保我们的测试可预测的方式运行,我们不依赖于外部因素。
  • 使用测试机器人。这使得我们的测试容易阅读,使我们在应用程序代码更改为快速更新我们的测试。

如果您有关于测试的任何疑问,请与我联系上推特

在一个小团队正在实施一系列的像这个伟大的新做法的工作感兴趣吗?我们正在招聘!

资源

要查看所有已完成的代码从这个博客,我已经放在每个文件到一个要点:https://gist.github.com/AdamMc331/1362feb28241ebfac320c21f6e24f341