# 任务 3：Redux 数据流测试

Hey，你好，我是 JimmyLv 吕靖，这是你开始 前端 TDD 练习的第 17 天。今天你将会开始进入 Redux Store，对 Redux 的数据流进行测试，驱动出更好的状态管理，把代码放在该放的位置。

在上一个任务当中你已经用上了 React Router 的几个官方 Hook，其实，你会发现，你写 React 的思路就变成了：先驱动出组件树拆分，组件就是函数，那这个函数可能时不时需要来点儿副作用，比如说跳转一下路由，或者获取一下数据，那就用钩子（Hook）把外部代码"钩"进来。

也许你之前已经用上了 `useState` 来做一些简单的状态管理，但是你现在需要一个全局的状态管理器来进行状态管理，此时就不能直接去 `set` 某个 state 的值啦。于是乎，`useDispatch` 就被拿来触发（dispatch）一个 action，然后再去 Redux store 改变 state 的值：

```javascript
import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'counter/increment' })}>Increment counter</button>
    </div>
  )
}
```

然后当你需要某个状态的时候，便可以通过 `useSelector` 再从 Redux store 取回来，非常方便的同时，selector 还能帮你做一次获取数据的缓存，如果是同样的输入参数，因为是纯函数就必然会计算返回同样的结果，所以干脆就直接返回上一次的结果得了。

```javascript
import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = (props) => {
  const todo = useSelector((state) => state.todos[props.id])
  return <div>{todo.text}</div>
}
```

## 练习目标

* 使用 `redux-saga` 处理数据请求等异步 Action
* 先写 `redux-saga-test-plan` 测试，再写 Redux model 数据逻辑
* 学习 Jest Mock API 及异步测试方法 (`mock()` `fn()` `spyOn()`)
* 测试 selectors 与 `useSelector`，最简单的纯 JavaScript 逻辑单测

### 使用 `redux-saga-test-plan` 集成测试 Redux store

```javascript
import { expectSaga } from 'redux-saga-test-plan'
import * as service from '../services/book'
import reducer, { sagas, types } from './book'

jest.mock('../services/book')

test('should fetch book list', async () => {
  const payload = { category: '文学' }
  service.getAllBooks.mockResolvedValue([{ name: '你不知道的JavaScript' }])

  const result = await expectSaga(sagas)
    .withReducer(reducer) // reduce state update
    .dispatch({ type: types.FETCH, payload }) // dispatch action
    .run()

  expect(service.getAllBooks).toBeCalledWith(payload)
  expect(result.storeState).toEqual({
    list: [{ name: '你不知道的JavaScript' }],
    total: 1,
    error: null,
  })
})
```

### 如何测试 connect 到 Redux 的 React 组件

```javascript
import userEvent from '@testing-library/user-event'
import { createMemoryHistory } from 'history'
import { renderWithReduxAndRouter } from '../utils/testHelpers'
import { categoryTabs } from './CategoryTabs.stories'

const mockDispatch = jest.fn()
jest.mock('react-redux', () => ({
  useDispatch: () => mockDispatch,
}))

test('should show category list and fetch books by category', () => {
  const history = createMemoryHistory()
  const { queryByText } = renderWithReduxAndRouter(categoryTabs(), { history })

  userEvent.click(queryByText('文学'))

  expect(mockDispatch).toBeCalledWith({
    type: 'book/fetch',
    payload: {
      category: '文学',
    },
  })
  expect(history.location.search).toBe('?category=文学')
})

test('should show "文学" category book when select "文学" tab', () => {})
```

## 本次任务的练习要求

* `redux-saga` over `redux-thunk`（利于测试）
* 组件化拆分，不能有重复逻辑（通过 React Hook 提取）
* 实现 Redux 数据流，并保持 Outside-In TDD 的节奏
* 别忘了 Cypress E2E 测试还在运行着呢！

## 也请你思考一下

### 什么时机，回到 Cypress 检查页面元素是否正确显示？

```javascript
describe('Home Page', () => {
  beforeEach(() => {
    cy.visit('/home')
  })
  it('should show book list by category and tag', () => {
    // todo: should update to testId from plain text?
    cy.contains('书架').click()
    cy.contains('Bookshelf')

    cy.contains('编程').click()
    cy.contains('Java编程思想')

    cy.contains('JavaScript 1本').click()
    cy.contains('Java编程思想')
    cy.contains('你不知道的JavaScript')

    cy.contains('文学').click()
    cy.contains('平凡的世界')
  })
})
```

如果有必要的话，你还可以检查一下是否调用后端 API，但我建议在 E2E 层面只关心真实用户会怎么用你的网页，普通用户可能并不关心浏览器是否发送 HTTP request。

```javascript
describe('Home page to fetch books API', () => {
  beforeEach(() => {
    cy.visit('/home')
  })
  const initialBooks = [] // [...]

  const getItems = () => cy.request('/api/books').its('body')

  it('should fetch all books', () => {
    // before the request goes out we need to set up spying
    // see https://on.cypress.io/network-requests
    cy.server()
    cy.route('GET', '/api/books').as('book')
    cy.wait('@book').should('have.property', 'status', 200)
    cy.get('@book').its('response.body').should('deep.equal', {
      title: 'example post',
      body: 'this is a post sent to the server',
      userId: 1,
    })
  })
})
```


---

# 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/00-project-bookshelf/04-testing-redux-data-flow.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.
