React Hook测试指南

React为什么需要Hook中我们探讨了React为什么需要引入Hook这个属性,在React Hook实战指南中我们深入了解了各种Hook的详细用法以及会遇到的问题,在本篇文章中我将带大家了解一下如何通过为自定义hook编写单元测试来提高我们的代码质量,它会包含下面的内容:

  • 什么是单元测试
    • 单元测试的定义
    • 为什么需要编写单元测试
    • 单元测试需要注意什么
  • 如何对自定义Hook进行单元测试
    • Jest
    • React-hooks-testing-library
    • 例子

什么是单元测试

单元测试的定义

要理解单元测试,我们先来给测试下个定义。用最简单的话来说测试就是:我们给被测试对象一些输入(input),然后看看这个对象的输出结果(output)是不是符合我们的预期(match with expected result)。而在软件工程里面有很多不同类型的测试,例如单元测试(unit test),功能测试(functional test),性能测试(performance test)和集成测试(integration test)等。不同种类的测试的主要区别是被测试的对象和评判指标不一样。对于单元测试,被测试的对象是我们源代码的独立单元(individual unit),在面向过程编程语言(procedural programming)里面,单元就是我们封装的方法(function),在面向对象的编程语言(object-oriented programming)里面单元是类(class)的方法(method),我们一般不推荐将某个类或者某个模块直接作为单元测试的单元,因为这会使被测试的逻辑过于庞大,而且问题出现时不容易进行定位。

为什么需要编写单元测试

了解了单元测试的定义后,我们再来探讨一下为什么我们要在代码里面进行单元测试。

我们之所以要在项目中编写单元测试,主要是因为对代码进行单元测试有下面这些好处:

提高代码质量

单元测试可以提高我们的代码质量主要体现在它可以在我们开发某个功能的时候提前帮我们发现自己编写的代码的bug。举个例子,假如A同学写了一个叫做useOptions的hook它接受一个叫做options的参数,这个参数既可以是一个对象也可以是一个数组。A同学自己开发的过程中他只试过给useOptions传对象而没有试过给它传数组。同一个项目的B同学在使用useOptions的时候给它传了个数组发现代码挂了,这个时候B同学就得找A同学确认并等待A同学修复这个问题,这不但会影响B同学的开发进度而且还会让B同学觉得A同学不靠谱,或者觉得A同学的代码很烂。如果A同学有对useOptions进行单元测试的话,这个悲剧可能就不会发生了,因为A同学在为useOptions编写单元测试的时候就考虑了options为数组的情况,并且在B同学使用之前就修复了这个问题。因此编写单元测试可以让我们在开发的过程中提前考虑到很多后面使用才会发现的问题,进而提高我们的代码质量。

方便代码重构和新功能添加

编写单元测试的过程其实是我们给代码编写使用说明书的过程(specification)。这个使用说明书十分重要,它相当于代码生产者(producer)与代码消费者(consumer)之间的合约(contract),生产者需要保证在消费者使用代码没错的前提下代码要有使用说明书上面的效果。这其实会对代码生产者起到一定的制约作用,因为生产者必须保证无论是给原来的代码添加新的功能还是对它进行重构,它都要满足原来使用说明书上的要求。

继续上面那个例子,A同学和B同学都在项目的1.0.0版本中使用了useOptions这个hook,虽然useOptions没有编写单元测试,可是代码是没有bug的(最起码没有被发现)。后面项目需要进行2.0.0版本的升级了,这时候A同学需要为useOptions添加新的功能,A同学在改动了useOptions的代码后,在自己使用到的地方(对象作为参数的地方)做了测试,没有发现bug。在A同学自测完代码后,并将这个更改集成(integration)到了项目的master分支上。后面B同学在更新完A同学的代码后,发现自己的代码出现了一些问题,这个时候B同学很可能就会手忙脚乱,并且可能需要花费一段时间才能定位到原来是A同学对useOptions的改动影响到他的功能,这除了会影响到项目的进度外还会让A同学和B同学的关系进一步恶化。这个悲剧同样也是可以通过编写单元测试来避免的,试想一下假如A同学有给useOptions编写配套的使用说明书(单元测试),A同学在改动完代码后,它的代码是通过不了使用说明书的检查的,因为它的改动改变了useOptions之前定义好的外部行为,这个时候A同学就会提前修复自己的代码进而避免了B同学后面的苦恼。通过这个例子大家可能还是没有体会到单元测试对于我们平时产品迭代或者代码重构的重要性,可是你试想一下在一个比较大的项目中是有很多个A同学和B同学的,也有成千上万个useOptions函数,当真的发生类似问题的时候bug将会更难被定位和修复,如果我们大部分的代码都有单元测试的话,无论是对代码增加新的功能还是对原来的代码进行重构我们都会更有信心。

完善我们代码的设计

在软件工程里面有个概念叫做测试驱动开发(Test-driven Development),它鼓励我们在实际开始编码之前先为我们的代码编写测试用例。这样做的目的是让我们在开发之前就以代码使用者的角度去评判我们的代码设计。如果我们的代码设计很糟糕,我们就会发现我们很难为它们编写详尽的单元测试用例,相反如果我们的代码设计得很好(低耦合高内聚),各个函数的参数和功能都设计得十分合理,我们就十分容易就为它们编写对应的单元测试。我们要记住一句话:高质量的代码一定是可以被测试的(testable)。那么为什么是在还没开始写代码之前就编写测试用例呢?这是因为如果我们在代码写完之后再编写测试的话,即使我们发现代码设计得再不合理,我们也没有动力去改了,因为对设计的改动可能会让我们重写所有的代码,所以我们需要在实际编码之前进行单元测试的编写,因为这个时候的改代码阻力是最小的。

提供文档功能

我们在为代码编写单元测试的时候实际上是在为代码编写一个个使用例子,因此别的开发者在使用我们代码的时候可以通过我们的单元测试来快速掌握我们定义的各种函数的用法。另外教大家一个实用的技巧:如果我们发现某个库的文档不是很全面的话,可以通过查看这个库的单元测试来快速掌握这个库的用法。

单元测试需要注意的问题

隔离性

上面我们说到单元测试是对代码独立的单元进行测试,这个独立的意思不是说这个函数(单元)不会调用另外一个函数(单元),而是说我们在测试这个函数的时候如果它有调用到其它的函数我们就需要mock它们,从而将我们的测试逻辑只放在被测试函数的逻辑上,不会受到其它依赖函数的影响。举个例子我们现在要测试以下函数:

async function fetchUserDetails(userId) {
   
  const userDetail = await fetch(`https://myserver.com/users/${
     userId}`)
  return userDetail
}

在测试fetchUserDetails时我们就需要mock fetch这个函数了,因为我们现在测试的函数是fetchUserDetails,我们只需要确定在外界调用fetchUserDetails的时候fetch会被调用,并且调用的参数是“https://myserver.com/users/${userId}”就行了,至于fetch函数如何发请求和处理返回来的数据都是fetch函数自己的事,我们不应该在测试fetchUserDetails的时候关心这个问题。

单元测试要注意隔离性的另外一个原因是它可以保证当测试案例失败的时候我们可以十分容易定位到问题的所在。以上面的代码为例,如果我们没有mock fetch函数,一旦我们的测试失败,我们很难分清是fetchUserDetails逻辑错了还是fetch的逻辑错了。

可重复性

我们编写的所有单元测试用例一定不能依赖外部的运行环境,否则我们的单元测试将不具备可重复性(repeatable)。所谓的可重复性就是:如果我们的单元测试用例现在是可以通过的,那么在代码不发生变动和测试用例没有改变的前提下它将是一直可以通过的。举个测试用例不具备可重复性的例子,假如你将项目的单元测试数据全部放在数据库里面,你今天运行项目的测试用例是可以通过的,而第二天其他人无意改了数据库的数据,这个时候你的测试用例就通过不了了,我们就说这些测试用例不具备可重复性,出现这个问题的主要原因是它们使用了外部的依赖作为测试条件。由此可见要使我们的测试用例具备可重复性的一个关键点是在编写单元测试的时候避免外部依赖,这些外部依赖包括数据库网络请求本地文件系统等。

另外一个影响到测试用例可重复性的一个重要的却容易被忽略的因素是:不同单元测试用例之间共用了一些测试数据,某个测试用例对测试数据的更改可能会影响其它测试用例的正确执行。因此我们在编写单元测试用例的时候一定要避免不同测试用例之间共用一些测试数据,尽量将每个测试用例隔离起来。

提高代码覆盖率

在单元测试里面有个概念叫做代码覆盖率(test coverage),它表明我们代码被测试的程度。举个例子假如我们有一个100行的函数,在我们运行完所有的为这个函数编写的单元测试用例之后,如果测试框架告诉我们这个函数的覆盖率是80%,这表明我们的测试用例代码只覆盖了这个函数的80行代码,还有一些代码分支(if/else, switch, while)没有被执行到。如

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值