Hey,你好,我是 JimmyLv 吕靖,这是你开始 前端 TDD 练习的第 17 天。今天你将会开始进入 Redux Store,对 Redux 的数据流进行测试,驱动出更好的状态管理,把代码放在该放的位置。
在上一个任务当中你已经用上了 React Router 的几个官方 Hook,其实,你会发现,你写 React 的思路就变成了:先驱动出组件树拆分,组件就是函数,那这个函数可能时不时需要来点儿副作用,比如说跳转一下路由,或者获取一下数据,那就用钩子(Hook)把外部代码"钩"进来。
也许你之前已经用上了 useState
来做一些简单的状态管理,但是你现在需要一个全局的状态管理器来进行状态管理,此时就不能直接去 set
某个 state 的值啦。于是乎,useDispatch
就被拿来触发(dispatch)一个 action,然后再去 Redux store 改变 state 的值:
复制 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 还能帮你做一次获取数据的缓存,如果是同样的输入参数,因为是纯函数就必然会计算返回同样的结果,所以干脆就直接返回上一次的结果得了。
复制 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
复制 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 组件
复制 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 检查页面元素是否正确显示?
复制 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。
复制 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,
})
})
})