原文: How to build sturdy React apps with TDD and the React Testing Library

在我开始学习 React 时,我曾经挣扎于如何以一种既有用又直观的方式测试我的 web 应用。每次我想要测试一个组件时,我都会使用 EnzymeJest 进行表层渲染。

当然,我绝对是在滥用快照测试功能。

好吧,至少我写了一个测试,对吧?

你可能在某个地方听说过,编写单元测试和集成测试将提高你编写的软件的质量。另一方面,糟糕的测试会滋生虚假的信心。

最近,我参加了 workshop.me 上由 Kent C. Dodds 主持的一个研讨会,他教我们如何为 React 应用编写更好的集成测试。

他还指导我们使用他的新的测试库,以强调以用户可能遇到的相同方式测试应用程序。

在本文中,我们将通过创建评论反馈来学习如何在构建稳定的 React 应用程序中运用 TDD。当然,这个过程适用于几乎所有的软件开发,而不仅仅是 React 或 JavaScript 应用。

开始

我们将首先运行 create-react-app 并安装依赖项。我的假设是,如果你正在阅读关于测试应用程序的文章,你可能已经熟悉安装和启动 JavaScript 项目。在这里,我将使用 yarn 而不是 npm。

create-react-app comment-feed
cd comment-feed
yarn

首先,我们将删除 src 目录中除 index.js 之外的所有文件。然后,在 src 文件夹内部,创建一个名为 components 的新文件夹和另一个名为 containers 的文件夹。

在测试工具方面,我将使用 Kent 的 React 测试库 构建此应用程序。它是一款轻量级的测试工具,鼓励开发者以实际使用时相同的方式测试他们的应用程序。

与 Enzyme 一样,它导出一个渲染函数,但这个渲染函数始终对你的组件进行完整挂载。它导出辅助方法,允许你通过标签、文本甚至测试 ID 来定位元素。Enzyme 也通过其 mount API 实现了这一点,但它创建的抽象实际上提供了更多选项,其中许多选项允许你摆脱测试实现细节。

我们不想要测试所有的实现细节。我们想要渲染一个组件,看看当点击或更改 UI 上的某些内容时是否会发生正确的事情。就是这样!不再直接检查 props 、state 或类名。

现在让我们安装它们并开始工作。

yarn add react-testing-library

通过 TDD 构建评论反馈

让我们以 TDD 风格进行第一个组件的开发。启动你的测试运行器。

yarn test --watch

containers 文件夹中,我们将添加一个名为 CommentFeed.js 的文件。与之相伴的,添加一个名为 CommentFeed.test.js 的文件。在第一个测试中,让我们验证用户是否可以创建评论。太早了?好吧,既然我们还没有任何代码,我们将从一个较小的测试开始。检查一下是否可以渲染反馈。

关于 react-testing-library 的一些说明

首先,让我们注意这里的渲染函数。它类似于 react-dom 将组件渲染到 DOM 的方式,但它返回一个对象,我们可以解构该对象以获得一些实用的测试辅助工具。在这种情况下,我们得到 queryByText,它会返回我们期望在 DOM 上看到的 HTML 元素。

React 测试库文档提供了一个层次结构,帮助你决定使用哪个查询或获取方法。通常,顺序如下:

  • getByLabelText(表单输入)
  • getByPlaceholderText(仅在你的输入没有标签时使用 — 很少使用!)
  • getByText(按钮和标题)
  • getByAltText(图片)
  • getByTestId(用于动态文本或其他你想要测试的奇怪元素)

每个方法都有一个相关的 queryByFoo,除了在找不到元素时不会使测试失败之外,它们的功能相同。如果你只是测试元素的存在,请使用这些方法。

如果这些方法都无法满足你的需求,render 方法还返回映射到 container 属性的 DOM 元素,因此你可以像 container.querySelector(‘body #root’) 这样使用它。

首次实现代码

现在,实现看起来相当简单。我们只需要确保“评论反馈”是一个组件。

它可能会更糟糕 - 我的意思是,我在编写整篇文章的过程中,还要编写组件的样式。幸运的是,测试并不太关心样式,所以我们可以专注于应用逻辑。

接下来的测试将验证我们是否可以渲染评论。但是我们甚至还没有任何评论,所以也添加一个组件,在测试之后添加。

我还将创建一个 props 对象来存储我们可能在这些测试中重用的数据。

在这种情况下,我正在检查评论的数量是否等于传入 CommentFeed 的项目数量。这是无关紧要的,但测试失败给了我们创建 Comment.js 文件的机会。

这为我们的测试套件亮起了绿灯,我们就可以放心地继续了。向 TDD 致敬。当我们给它一个空数组时,它当然会工作。但是,如果我们给它一些真实的对象,会发生什么呢?

我们必须更新实现以实际渲染内容。现在我们知道要去哪里,这很简单,对吧?

看看这个,我们的测试再次通过了。这是一个美妙的截图。

1*vGkFKnUkA9ms5PbaOWoQ_A

注意我从未说过我们应该用 yarn start 启动程序吗?我们继续保持这种方式一段时间。关键是,你必须用心去感受代码。

样式只是外部表现 - 重要的是内部的东西。

以防你想启动应用程序,将 index.js 更新为以下内容:

添加评论表单

这是事情开始变得更有趣的地方。这是我们从困倦地检查 DOM 节点的存在到实际使用它并验证行为的地方。所有其他的东西都是热身。

让我们从描述我想要的表单开始。它应该:

  • 包含一个作者的文本输入
  • 包含一个评论条目的文本输入
  • 有一个提交按钮
  • 最终调用 API 或处理创建和存储评论的其他服务。

我们可以在一个集成测试中完成这个列表。对于之前的测试用例,我们进行得相当缓慢,但现在我们要加快速度,一举完成。

注意我们的测试套件是如何发展的吗?我们从在各自的测试用例中硬编码 props 转变为为它们创建一个工厂。

准备、执行、断言

以下集成测试可以分为三个部分:准备,执行和断言。

  • 准备: 为测试用例创建props和其他测试用例
  • 执行: 模拟对元素的更改,例如文本输入或按钮点击
  • 断言: 断言所需的函数被正确次数调用,并使用正确的参数

关于代码,我们做了一些假设,比如我们的标签命名或我们将拥有一个 createComment prop。

在查找输入时,我们希望尝试通过它们的标签找到它们。这样在构建应用程序时,可以优先考虑可访问性。使用 container.querySelector 是获取表单的最简单方法。

接下来,我们必须为输入分配新值,并模拟更改以更新它们的状态。这一步可能感觉有点奇怪,因为通常一次输入一个字符,为每个新字符更新组件的状态。

这个测试的行为更像是复制/粘贴的行为,从空字符串变为 “Socrates” 。目前没有中断问题,但我们可能需要注意一下,以防以后出现问题。

在提交表单后,我们可以对诸如调用了哪些 props 以及使用了哪些参数等事项进行断言。我们还可以利用这个时刻来验证表单输入是否已清除。

这让人望而生畏吗?不要害怕。首先将表单添加到渲染函数中。

我可以将这个表单分解成一个单独的组件,但现在我会保持不变。相反,我会将其添加到我桌子旁边的“重构愿望清单”中。

这是 TDD 的方式。当某件事看起来可以重构时,做个记录然后继续。只有在抽象的存在对你有益且不感到多余时才进行重构。

还记得我们通过创建 createProps 工厂来重构测试套件的时候吗?就像那样。我们也可以重构测试。

现在,让我们添加 handleChangehandleSubmit 类方法。当我们更改输入或提交表单时,这些方法会被触发。我还将初始化状态。

这样做就可以了。我们的测试通过了,我们有一些类似于真实应用程序的东西。我们的覆盖率看起来如何?

1*Q4coAIT2yaP120pDWGxoAQ

还不错。如果我们忽略 index.js 中的所有设置,我们就有一个完全覆盖的 Web 应用程序,至少在执行的行数方面是这样。

当然,为了验证应用程序是否按照我们的意图工作,我们可能还需要测试其他用例。覆盖率的数字只是你的老板在谈论其他同事时可以炫耀的东西。

点赞评论

如何检查我们可以点赞评论呢?这可能是在我们的应用程序中建立某种身份验证概念的好时机。但我们现在还不会跳得太远。首先,让我们更新 props 工厂,为生成的评论添加一个 auth 字段和 ID。

“认证”用户将通过应用程序传递其 auth 属性。与他们是否经过身份验证相关的任何操作都将被记录。

在许多应用程序中,此属性可能包含在向服务器发出请求时发送的某种访问令牌或 cookie 中。

在客户端,此属性的存在使应用程序知道它们可以让用户查看其个人资料或其他受保护的路由。

然而,在这个测试示例中,我们不会过多地处理身份验证。想象这样一个场景:当你进入聊天室时,你提供网名。从那时起,你将负责使用此网名的每个评论,尽管其他人也使用同样的名称登录。

虽然这不是一个很好的解决方案,即使在这个人为的例子中,我们只关心 CommentFeed 组件的行为是否符合预期。我们不关心用户如何登录。

换句话说,我们可能有一个完全不同的登录组件来处理特定用户的身份验证,以获得让用户在我们的应用程序中大显身手的全能 auth 属性。

让我们“喜欢”一条评论。添加下一个测试用例,然后更新 props 工厂以包含 likeComment

现在,对于实现,我们将首先更新 Comment 组件,使其具有一个点赞按钮以及一个 data-testid 属性,以便我们可以找到它。

我将测试 ID 直接放在按钮上,以便我们可以立即模拟对它的点击,而无需嵌套查询选择器。我还在按钮上附加了一个 onClick 处理程序,以便它传递给它的 onLike 函数。

现在我们只需将此类方法添加到我们的 CommentFeed。

你可能想知道为什么我们不直接将 likeComment 通过 prop 传递给 Comment 组件。为什么我们要将其作为类属性?

在这种情况下,因为它相当简单,我们不必构建这个抽象。将来,我们可能会决定添加其他 onClick 处理程序,例如处理分析事件或启动对该帖子评论的订阅。

在此容器组件的 handleLike 方法中捆绑多个不同的函数调用具有其优势。如果我们愿意,在成功“点赞”后,我们还可以使用此方法更新组件的状态。

不喜欢评论

到目前为止,我们已经有了渲染、创建和喜欢评论的工作测试。当然,我们还没有实现实际执行此操作的逻辑——我们没有更新存储或写入数据库。

你可能还注意到,我们正在测试的逻辑很脆弱,不太适用于真实世界的评论反馈。例如,如果我们尝试喜欢我们已经喜欢的评论,会发生什么?它会无限地增加喜欢的计数,还是会取消喜欢?我可以喜欢我自己的评论吗?

我将把组件的功能扩展留给你的想象,但一个好的开始是编写一个新的测试用例。这里有一个基于我们想要实现不喜欢已经喜欢的评论的假设。

请注意,我们正在构建的评论反馈允许我喜欢我自己的评论。谁会这样做?

我已经更新了 Comment 组件,添加了一些逻辑来确定当前用户是否喜欢该评论。

好吧,我有点作弊:在我们之前将 author 传递给 onLike 函数的地方,我改为 currentUser ,这是传递给 Comment 组件的 auth 属性。

毕竟,当其他人喜欢他们的评论时,评论的作者出现在那里是没有意义的。

我之所以意识到这一点,是因为我一直在努力编写测试。如果我只是偶尔编写代码,这一点可能会被我忽略,直到我的一位同事斥责我的无知。

但是这里没有无知,只有测试和随之而来的代码。确保更新 CommentFeed,以便它期望传递 auth 属性。对于 onClick 处理程序,我们可以省略传递 auth 属性,因为我们可以从父级的 handleLikehandleDislike 方法中获取 auth 属性。

总结

希望你的测试套件看起来像一棵未点亮的圣诞树。

我们可以采取很多不同的方法,这可能会让人有点不知所措。每当你想到某个想法时,只需将其写下来,无论是在纸上还是在新的测试块中。

例如,假设你希望在一个单独的类方法中实现 handleLikehandleDislike,但你现在有其他优先事项。你可以通过在测试用例中编写文档来实现这一点:

这并不意味着你需要编写一个全新的测试。你也可以更新前两个案例。但关键是,你可以将测试运行器用作应用程序更加紧迫的“待办事项“列表。

有用的链接

有一些很棒的内容涉及到大规模的测试。以下是一些特别启发了本文以及我自己实践的内容。

我希望这些能帮你处理测试问题。

想了解更多文章或机智的评论吗?如果你喜欢这篇文章,请给我一些掌声,并在 MediumGithubTwitter 上关注我!