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

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

![XP-feedback](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4lBzc0WTiIyV1pQqv6%2F-M4lC00SJH_AwtAc43fi%2FXP-feedback.png?generation=1586742189761495\&alt=media)

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

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

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

## 练习目标

* [ ] 使用 Testing Library 测试驱动组件接口设计
* [ ] 完成第一部分业务：C 端购物车的静态+动态部分
* [ ] 使用 React Hook 重构组件状态管理及其交互行为

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

## 本次任务的练习要求

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

* 没有单元测试，不实现任何功能代码；
* 只编写仅能代表一种失败情况的测试代码；
* 只编写恰好能通过单元测试的产品代码。

![TDD Red-Green-Refactor Cycle](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4lBzc0WTiIyV1pQqv6%2F-M4lC00UU4ygCE_IEBU_%2Ftdd-red-green-refactor.png?generation=1586742190871817\&alt=media)

```javascript
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 洞见](https://insights.thoughtworks.cn/practical-test-pyramid/)）

![测试金字塔](https://cdn.jsdelivr.net/gh/JimmyLv/images/2018/20181030211424.png)
