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

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

![XP-feedback](/files/-M4lC00SJH_AwtAc43fi)

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

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

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

## 练习目标

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

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

## 本次任务的练习要求

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

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

![TDD Red-Green-Refactor Cycle](/files/-M4lC00UU4ygCE_IEBU_)

```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)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jimmylv.gitbook.io/tdd-frontend/coding/01-project-shoppingcart/05-unit-test-driven-component-development.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
