任务 2:综合应用 - 驱动组件接口设计

Hey,你好,我是 JimmyLv 吕靖,这是你开始 前端 TDD 练习的第 20 天,昨天你已经对 ShoppingCart 项目中的会用到的组件进行了拆分,并且基于 E2E 层面的测试已经开始实现需求了,有可能你已经遇到了不少问题。等一等,今天我想告诉你的是,组件级别的单元测试驱动开发才是最核心、最重点的 TDD 实践。

在 XP 极限编程提到的反馈环中我们可以看出,除了结对编程以外,单元测试是我们开发者最好的反馈工具。

既然单元测试应该由开发者,在开发软件的同时编写对应的单元测试。它应该是内建的,而不是后补的:即在编写实现的同时完成单元测试,而不是写完代码再一次性补足。测试先行,这正是 TDD(测试驱动开发)的做法。使用 TDD 开发方法是得到可靠单元测试的唯一途径。

测试很难补,其实补出来的测试几乎不可能完整覆盖我们对重构和质量的要求。TDD 和单元测试是全有或全无:不做 TDD,难以得到好的单元测试;TDD 是获得可靠的单元测试的的唯一途径。除此之外别无捷径,想抛开 TDD 而获得一个好的单元测试是迷思,难以成功,接下来你就将练习如何从组件级别使用正确的单元测试,驱动出合理的组件接口设计。

练习目标

Testing Library 会帮助你测试单个组件的行为,通过测试驱动开发所有的组件 props,单元测试的重点也就在于用户如何与该组件进行交互,最终使得整个 E2E 测试通过。

本次任务的练习要求

TDD(测试驱动开发)的三大规则如下,遵守它便能够时刻给予开发者反馈,从而坚持下去:

  • 没有单元测试,不实现任何功能代码;

  • 只编写仅能代表一种失败情况的测试代码;

  • 只编写恰好能通过单元测试的产品代码。

import React from 'react'
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ProductList from './ProductList'
import fakeProducts from '../fixtures/product'

describe('<ProductList /> Component', () => {
  test('should show empty content when no products', () => {
    const comp = render(<ProductList products={[]} />)

    expect(comp.queryByText('购物车为空')).toBeTruthy()
  })

  test('should increase product when click "+" button', () => {
    // given
    const handleProductChange = jest.fn()
    const comp = render(<ProductList products={fakeProducts} handleProductChange={handleProductChange} />)

    // when
    expect(comp.queryByTestId('product-count')).toHaveTextContent('2')
    userEvent.click(comp.queryByTestId('increase-count'))

    // then
    expect(comp.queryByTestId('product-count')).toHaveTextContent('3')
    expect(handleProductChange).toBeCalledWith({
      code: 'ITEM001',
      count: 3,
      price: 100,
    })
  })

  test('should decrease product when click "-" button and can not decrease when count is 1', () => {
    // given
    const handleProductChange = jest.fn()
    const comp = render(<ProductList products={fakeProducts} handleProductChange={handleProductChange} />)

    // when
    expect(comp.queryByTestId('product-count')).toHaveTextContent('2')
    userEvent.click(comp.queryByTestId('decrease-count'))

    // then
    expect(comp.queryByTestId('product-count')).toHaveTextContent('1')
    expect(handleProductChange).toBeCalledWith({
      code: 'ITEM001',
      count: 3,
      price: 100,
    })

    userEvent.click(comp.queryByTestId('decrease-count'))
    expect(handleProductChange).not.toBeCalled()
  })
})

也请你思考一下

在前端 React/Vue 应用的单元测试中,对不同层级的 UI 组件单元测试有何不同?颗粒度该细到什么样的程度呢?

通常来说,在单元测试中我们希望将重点放在作为独立单元进行测试的组件上,并避免间接断言其子组件的行为。此外,对于包含许多子组件的组件,整个 render 树会变得非常之大,而反复 render 所有的子组件可能会减慢单元测试的速度。

而根据 Mike Cohn 的测试金字塔中所提到的两件事:

  • 编写不同粒度的测试

  • 层次越高,你写的测试应该越少

为了维持金字塔形状,一个健康、快速、可维护的测试组合应该是这样的:写许多小而快的单元测试。适当写一些更粗粒度的测试,写很少高层次的端到端测试。注意不要让你的测试变成冰淇淋那样子,这对维护来说将是一个噩梦,并且跑一遍也需要太多时间。(via 测试金字塔实战 – ThoughtWorks 洞见

最后更新于