如何测试驱动开发 React 组件?

sxkk20081年前知识分享97

什么是 TDD

TDD(Test-driven development),就是测试驱动开发,是敏捷开发中的一项核心实践和技术,也是一种软件设计方法论。

它的原理就是在编写代码之前先编写测试用例,由测试来决定我们的代码。而且 TDD 更多地需要编写独立的测试用例,比如只测试一个组件的某个功能点,某个工具函数等。

TDD 的过程

  • 编写测试用例
  • 运行测试,测试失败
  • 修改代码
  • 测试通过
  • 重构/优化代码
  • 新增功能,重复上述步骤

image.png

在某种程度上,它可能在初学者看来是单调乏味或者不切实际的,但是严格按照这个步骤来做这件事,让你自己决定测试用例是否对你的组件有帮助,会让测试用例变得有意义。

本文将以创建一个 Confirmation 组件来说明,如何在 React 中如何实现测试驱动开发。

Confirmation 组件的特点:

  • Confirmation 标题
  • 确认描述 —— 接收外部程序想要确认的问题
  • 一个确认的按钮,支持外部回调函数
  • 一个取消的按钮,支持外部回调函数

这两个按钮都不知道点击时接下来要做什么事,因为它超出了组件的职责范围,但是组件应该接收这些点击按钮的回调事件。先找个设计图:

image.png

那么,让我们开始吧。

测试组件

首先使用 create-react-app 初始化一个 react 项目。目前 cra 已经内置了 @testing-library/react 作为测试框架。

npx create-react-app my-react-app

我们先从测试文件开始。先创建了组件的目录“Confirmation” 并在其中添加一个“index.test.js”文件。

确保渲染测试

第一个测试相当抽象。仅仅需要检查组件是否展现(任何东西) ,以确保这个组件是存在。但是实际上,我将要测试的组件还不存在。

首先通过 getByRole 方法 查找 role属性等于dialog能否文档中找到。

role 属性可能不太常用, 当现有的 HTML 标签不能充分表达语义性的时候,就可以借助 role 来说明. 例如点击的按钮,就是 role="button" ;会让这个元素可点击;也可以使用 role 属性告诉辅助设备(如屏幕阅读器)这个元素所扮演的角色。

import React from 'react'
import { render } from '@testing-library/react'

describe('Confirmation component', () => {
  it('should render', () => {
    const { getByRole } = render(<Confirmation />)
    expect(getByRole('dialog')).toBeInTheDocument()
  })
})

运行测试并且监听

yarn test --watch

image.png

用 “脚趾头” 思考都知道这肯定是不能通过测试的。接下来,让我们创建一个足够满足这个测试的组件:

import React from 'react'

const Confirmation = () => {
  return <div role="dialog">div>
}

export default Confirmation

然后把这个组件导入到测试中,它现在通过了。

image.png

接下来,组件应该有一个动态标题。

动态标题测试

创建一个测试用例:

it('should have a dynamic title', () => {
  const title = '标题'
  const { getByText } = render(<Confirmation title={title} />)
  expect(getByText(title)).toBeInTheDocument()
})

测试失败了,修改代码使它通过:

import React from 'react'

const Confirmation = ({ title }) => {
  return (
    <div role="dialog">
      <h1>{title}h1>
    div>
  )
}

export default Confirmation

下一个特性,这个组件中存在一个确认问题提示

动态问题测试

这个问题也是动态的,这样它就可以从组件外部传入。

it('should have a dynamic confirmation question', () => {
  const question = 'Do you confirm?'
  const { getByText } = render(<Confirmation question={question} />)
  expect(getByText(question)).toBeInTheDocument()
})

测试再次失败,修改代码让它通过:

import React from 'react'

const Confirmation = ({ title, question }) => {
  return (
    <div role="dialog">
      <h1>{title}h1>
      <div>{question}div>
    div>
  )
}

export default Confirmation

确认按钮测试

接下来是确认按钮测试。我们首先要检查组件上是否有一个按钮,上面写着“确认”。

编写测试用例代码:

it('should have an "OK" button', () => {
  const { getByRole } = render(<Confirmation />)
  expect(getByRole('button', { name: '确认' })).toBeInTheDocument()
})

在这里使用 name 选项,因为我们知道这个组件中至少还有一个按钮,需要更具体地说明查找断言的是哪个按钮

组件代码:

import React from 'react'

const Confirmation = ({ title, question }) => {
  return (
    <div role="dialog">
      <h1>{title}h1>
      <div>{question}div>
      <button>确认button>
    div>
  )
}

export default Confirmation

取消按钮测试

同样对“取消”按钮做同样的事情:

测试:

it('should have an "取消" button', () => {
  const { getByRole } = render(<Confirmation />)
  expect(getByRole('button', { name: '取消' })).toBeInTheDocument()
})

组件代码:

import React from 'react'

const Confirmation = ({ title, question }) => {
  return (
    <div role="dialog">
      <h1>{title}h1>
      <div>{question}div>
      <button>确认button>
      <button>取消button>
    div>
  )
}

export default Confirmation

好了。现在我们得到了我们想要的组件渲染的 HTML ,现在我想要确保我可以从外部传递这个组件的按钮的回调函数,并确保它们在单击按钮时被调用。

那么我将从“确认”按钮的测试开始:

it('should be able to receive a handler for the "确认" button and execute it upon click', () => {
  const onOk = jest.fn()
  const { getByRole } = render(<Confirmation onOk={onOk} />)
  const okButton = getByRole('button', { name: '确认' })

  fireEvent.click(okButton)

  expect(onOk).toHaveBeenCalled()
})

先用 jest.fn 创建一个模拟函数,将其作为“onOk”处理函数传递给组件,模拟单击“确认”按钮,并断言函数已被调用。

image.png

这个测试显然失败了,下面是补充代码:

import React from 'react'

const Confirmation = ({ title, question, onOk }) => {
  return (
    <div role="dialog">
      <h1>{title}h1>
      <div>{question}div>
      <button onClick={onOk}>确认button>
      <button>取消button>
    div>
  )
}

export default Confirmation

最后,让我们对“取消”按钮做同样的事情:

测试:

it('should be able to receive a handler for the "取消" button and execute it upon click', () => {
  const onCancel = jest.fn()
  const { getByRole } = render(<Confirmation onCancel={onCancel} />)
  const okButton = getByRole('button', { name: '取消' })

  fireEvent.click(okButton)

  expect(onCancel).toHaveBeenCalled()
})

组件:

import React from 'react'

const Confirmation = ({ title, question, onOk, onCancel }) => {
  return (
    <div role="dialog">
      <h1>{title}h1>
      <div>{question}div>
      <button onClick={onOk}>确认button>
      <button onClick={onCancel}>取消button>
    div>
  )
}

export default Confirmation

下面是完整的测试文件:

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Confirmation from './index'

describe('Confirmation component', () => {
  it('should render', () => {
    const { getByRole } = render(<Confirmation />)
    expect(getByRole('dialog')).toBeInTheDocument()
  })

  it('should have a dynamic title', () => {
    const title = '标题'
    const { getByText } = render(<Confirmation title={title} />)
    expect(getByText(title)).toBeInTheDocument()
  })

  it('should have a dynamic confirmation question', () => {
    const question = 'Do you confirm?'
    const { getByText } = render(<Confirmation question={question} />)
    expect(getByText(question)).toBeInTheDocument()
  })

  it('should have an "确认" button', () => {
    const { getByRole } = render(<Confirmation />)
    expect(getByRole('button', { name: '确认' })).toBeInTheDocument()
  })

  it('should have an "取消" button', () => {
    const { getByRole } = render(<Confirmation />)
    expect(getByRole('button', { name: '取消' })).toBeInTheDocument()
  })

  it('should be able to receive a handler for the "确认" button and execute it upon click', () => {
    const onOk = jest.fn()
    const { getByRole } = render(<Confirmation onOk={onOk} />)
    const okButton = getByRole('button', { name: '确认' })

    fireEvent.click(okButton)

    expect(onOk).toHaveBeenCalled()
  })

  it('should be able to receive a handler for the "取消" button and execute it upon click', () => {
    const onCancel = jest.fn()
    const { getByRole } = render(<Confirmation onCancel={onCancel} />)
    const okButton = getByRole('button', { name: '取消' })

    fireEvent.click(okButton)

    expect(onCancel).toHaveBeenCalled()
  })
})

虽然这个组件没有样式,或者说我们还可以优化,添加跟多的功能,以上步骤已经充分展示了测试驱动开发的逻辑。

image.png

TDD 一步一步地引导完成组件特性的规范,确保我们在组件重构或者他人修改代码的时候能够遵循现有开发的逻辑。这这是 TDD 的优势。

调试

我们可以使用 debug 打印渲染的 html 结构

代码

it('should be able to receive a handler for the "取消" button and execute it upon click', () => {
  const onCancel = jest.fn()
  const { getByRole, debug } = render(<Confirmation onCancel={onCancel} />)

  debug()
})

image.png

这样可以方便我们查找 dom。

小结

当然 @testing-library/react 还有很多方便的 api。大家可以自行查阅。

image.png

未来可能会出一些文章关于测试的文章。例如:

如何出测试 react hooks ?

如何测试 react 路由?

如何测试接口?

希望这篇文章对大家有所帮助,也可以参考我往期的文章或者在评论区交流你的想法和心得,欢迎一起探索前端。

相关文章

油猴脚本开发教程

highlight: monokai前言如何跟普通朋友介绍前端工程师是一个怎样的职位? 我会毫不犹豫的给他的浏览器装上 Tampermonkey,再装一个去广告插件,他们肯定会觉得你很牛逼,然后再问问...

这是启动后的界面,这个 demo 不是一个简单的页面,而是一个包含了深度嵌套路由的例子。

下图我开发时的截图,Turbopack 直接在命令行中打印出了构建时间,我们看到启动时间只需要 2.3ms

更新速度

试着修改代码,程序会自动热更新,绝大多数次数更新时间都很快,但偶尔有几次更新时间却很长,图片中有一处需要 16s(我使用的是 Mac M1),这其中的原因就不得而知了,尤大也发布了测评,使用 1000 个节点来对比更新速度,数据显示:根组件与 vite 时间几乎相同,叶子节点比 vite 快 68%,与官方称比 vite 快 10 倍相差甚远。当然目前 Turbopack 还处于 alpha 阶段,期待 Turbopack 能够尽快推出正式版。

Turbopack 特点

  • 开箱即用 TypeScript, JSX, CSS, CSS Modules, WebAssembly 等
  • 增量计算: Turbopack 是建立在 Turbo 之上的,Turbo 是基于 Rust 的开源、增量记忆化框架,除了可以缓存代码,还可以缓存函数运行结果。
  • 懒编译:例如,如果访问 localhost:3000,它将仅打包 pages/index.jsx,以及导入的模块。

为什么不选择 Vite 和 Esbuild?

Vite 依赖于浏览器的原生 ES Modules 系统,不需要打包代码,这种方法只需要转换单个 JS 文件,响应更新很快,但是如果文件过多,这种方式会导致浏览器大量级联网络请求,会导致启动时间相对较慢。所以作者选择同 webpack 一样方式,打包,但是使用了 Turbo 构建引擎,一个增量记忆化框架,永远不会重复相同的工作。

Esbuild 是一个非常快速的打包工具,但它并没有做太多的缓存,也没有 HMR(热更新),所以在开发环境下不适用。

你好,Next.js 13

theme: vuepress highlight: monokai文章为稀土掘金技术社区首发签约文章,14 天内禁止转载,14 天后未获授权禁止转载,侵权必究!前言上周发布了 Next.js 的一个...

前端工具箱

丰富的前端工具,前端工程师开发小帮手!@maqibin浙ICP备17007919号-2© 2023 runjs.cool...

使用 react-pdf 打造在线简历生成器

前言PDF 格式是 30 年前开发的文件格式,并且是使用最广泛的文件格式之一,我们最喜欢使用它作为简历、合同、发票、电子书等文件的格式,最主要的原因是文档格式可以兼容多种设备和应用程序,而且内容 10...

小度AI:颠覆智能助手市场的黑马

小度AI:颠覆智能助手市场的黑马

  小度AI成为了话题的焦点。作为一款智能助手产品,小度AI在市场上引起了广泛的关注和讨论。其强大的人工智能技术和独特的用户体验,将可能给市场带来巨大的影响,甚至可能颠覆整个...

人工智能赋能——探索easydl平台在机器学习领域的应用与前景

人工智能赋能——探索easydl平台在机器学习领域的应用与前景

  近年来,随着人工智能技术的快速发展,机器学习成为了研究的热点。easydl平台作为一种强大的人工智能平台,为机器学习领域带来了许多新的机遇和挑战。本文将探索easydl平...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。