一、TDD (Test-Driven-Development )的认识
TDD 简单点来说就是测试驱动开发,这里说的测试并不是指开发团队中的测试小组来驱动开发者写代码,而是开发人员根据业务场景编写的单元测试文件,通过单元测试文件来写业务代码,直到业务代码都通过单元测试为止。
整个环节可以拆分为:
二、自己手动实践一个TDD
-
1、创建一个react 项目 npx create-react-app react-demo01 --template typescript
-
2、可以将react 配置映射出来,查看react 的测试包,正常项目是不需要反射出来的 "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
-
3、这里需要安装依赖包来对ts 的支持 # 因为使用的是ts需要安装
npm install @types/jest -D
-
4、这里直接使用bootstrap 来布局,最终要交付的产品如下图 根据业务来拆分组件
Message.tsx 是一个列表值的组件MessageList.tsx 是全部列表组件的集合MessageForm.tsx 是下面表单提交的组件MessageApp.tsx 是一个整体组件 -
5、先编写__tests__/Message.tsx 单元测试 业务 场景分析: 希望渲染一个li 标签,标签的类名是list-group-item 里面要有内容
import React from 'react';
import Message from './../../components/Message';
import { render, screen } from '@testing-library/react';
describe('测试Message组件', () => {
test('应该渲染一个li,类名是list-group-item', () => {
const message: string = 'hello word';
render(<Message message={message} />);
expect(screen.getByRole('item').nodeName.toLocaleLowerCase()).toBe('li');
expect(screen.getByRole('item').isEqualNode).toBeTruthy();
expect(screen.getByRole('item').className).toBe('list-group-item');
expect(screen.getByRole('item').textContent).toBe(message);
});
});
-
6、根据测试用例编写组件
import React, { PropsWithChildren } from 'react';
type IProps = PropsWithChildren<{
message: string;
}>;
const Message: React.FC<IProps> = (props: IProps) => {
const { message } = props;
return <li className='list-group-item' role='item'>{message}</li>;
};
export default Message;
-
7、src/__tests__/MessageList.tsx 组件
import React from 'react';
import { render, screen } from '@testing-library/react';
import MessageList from './../../components/MessageList';
describe('测试MessageList组件', () => {
test('测试当前组件是否为ul,并且类名是list-group', () => {
const messageList: string[] = ['hello', 'word'];
render(<MessageList messageList={messageList} />);
expect(screen.getByRole('ul').nodeName.toLocaleLowerCase()).toBe('ul');
expect(screen.getByRole('ul').className).toBe('list-group');
});
test('测试当前组件下面是否为li', () => {
const messageList: string[] = ['hello', 'word'];
render(<MessageList messageList={messageList} />);
expect(screen.getByRole('ul').className).toBe('list-group');
expect(screen.getByRole('ul').childNodes.length).toEqual(2);
expect(screen.getAllByRole('item').length).toEqual(2);
expect(screen.getByRole('ul').lastChild.nodeName.toLocaleLowerCase()).toBe(
'li'
);
});
});
import React, { PropsWithChildren } from 'react';
import Message from './Message';
type IProps = PropsWithChildren<{
messageList: string[];
}>;
const MessageList: React.FC<IProps> = (props: IProps) => {
const { messageList } = props;
return (
<ul className='list-group' role='ul' data-testid='list-group'>
{messageList.map((item: string, index: number) => {
return <Message key={index} message={item} />;
})}
</ul>
);
};
export default MessageList;
-
8、表单的 import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import MessageForm from './../../components/MessageForm';
describe('测试MessageForm组件', () => {
test('测试当前是结构', () => {
const addMessage = jest.fn();
render(<MessageForm addMessage={addMessage} />);
expect(screen.getByTestId('form').nodeName.toLocaleLowerCase()).toBe(
'form'
);
expect(screen.getByTestId('button')).toBeTruthy();
expect(
screen.getByTestId('form').getElementsByTagName('input')
).toBeTruthy();
expect(
screen.getByPlaceholderText('请输入留言内容').nodeName.toLocaleLowerCase()
).toBe('input');
});
test('测试表单的事件', () => {
const addMessage = jest.fn();
render(<MessageForm addMessage={addMessage} />);
const message: string = 'hello word';
fireEvent.change(screen.getByTestId('message'), {
target: { value: message },
});
fireEvent.click(screen.getByTestId('button'));
expect(addMessage).toBeCalled();
});
test('测试表单内容为空的时候不提交', () => {
const addMessage = jest.fn();
render(<MessageForm addMessage={addMessage} />);
const message: string = '';
fireEvent.change(screen.getByTestId('message'), {
target: { value: message },
});
(expect(screen.getByTestId('message')) as any).toHaveValue(message);
fireEvent.click(screen.getByTestId('button'));
expect(addMessage).not.toBeCalled();
});
});
import React, { PropsWithChildren, useRef } from 'react';
type IProps = PropsWithChildren<{
addMessage: (message: string) => void;
}>;
const MessageForm: React.FC<IProps> = (props: IProps) => {
const { addMessage } = props;
const inputRef = useRef<HTMLInputElement | null>(null);
const submitHandler = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
): void => {
event.preventDefault();
if (inputRef.current!.value) {
addMessage(inputRef.current!.value);
inputRef.current!.value = '';
}
};
return (
<form data-testid='form'>
<div className='form-group'>
<label htmlFor='message'>留言内容:</label>
<input
type='text'
className='form-control'
data-testid='message'
ref={inputRef}
placeholder='请输入留言内容'
/>
</div>
<button
type='button'
data-testid='button'
className='btn btn-primary'
style={{ width: '100%' }}
onClick={submitHandler}
>
提交
</button>
</form>
);
};
export default MessageForm;
-
9、同理MessageApp.tsx 的单元测试也类似
三、BDD(Behavior Driven Development) 行为驱动测试
BDD 将TDD 的一般技术和原理与领域驱动设计(DDD )的想法相结合。 BDD 是一个设计活动,您可以根据预期行为逐步构建功能块。简单来说就是你根据项目需求来先写摸测试用例的模板,然后撸代码,代码撸了完了,在根据测试用例模板来完善单元测试,最后用单元测试来验证你写的代码。
四、组件中有接口请求的测试
-
1、页面打开就加载数据 import React, { useState, useEffect } from 'react';
import axios from 'axios';
export interface IUser {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
const GitHubUser = () => {
const [userList, setUserList] = useState<IUser[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
axios.get('https://api.github.com/users?since=1').then((res) => {
setLoading(false);
setUserList(res.data);
});
}, []);
return (
<>
<div data-testid='loading'>
<span>{loading ? 'loading....' : ''}</span>
</div>
<ul>
{userList.map((item: IUser) => {
return <li key={item.id}>{item.login}</li>;
})}
</ul>
</>
);
};
export default GitHubUser;
import React from 'react';
import { render, screen } from '@testing-library/react';
import axios from 'axios';
import GitHubUser from './../../components/GitHubUser';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('测试axios请求', () => {
test('测试发起axios请求', async () => {
mockedAxios.get.mockResolvedValueOnce({
data: [
{ id: 1, login: 'hello' },
{ id: 2, login: 'word' },
],
});
render(<GitHubUser />);
const loadingText = await screen.findByTestId('loading');
(expect(loadingText) as any).toHaveTextContent('');
const userGroup = await screen.findByTestId('user-group');
(expect(userGroup) as any).not.toHaveTextContent();
});
});
-
2、模拟按钮点击去获取数据 import React, { useState } from 'react';
import axios from 'axios';
export interface IUser {
login: string;
id: number;
}
const GitHubUser = () => {
const [userList, setUserList] = useState<IUser[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const getData = () => {
setLoading(true);
axios.get('https://api.github.com/users?since=1').then((res) => {
console.log(res.data, '测试请求结果');
setLoading(false);
setUserList(res.data);
});
};
return (
<>
<button onClick={getData} data-testid='button'>
点击获取数据
</button>
<div data-testid='loading'>
<span>{loading ? 'loading....' : ''}</span>
</div>
<ul data-testid='user-group'>
{userList.map((item: IUser) => {
return (
<li key={item.id} data-testid='item'>
{item.login}
</li>
);
})}
</ul>
</>
);
};
export default GitHubUser;
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import axios from 'axios';
import GitHubUser from './../../components/GitHubUser';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('测试axios请求', () => {
test('模拟点击获取数据', async () => {
const data = [
{ id: 1, login: 'hello' },
{ id: 2, login: 'word' },
];
mockedAxios.get.mockImplementationOnce(() => Promise.resolve({ data }));
await act(async () => {
render(<GitHubUser />);
userEvent.click(screen.getByTestId('button'));
});
expect(screen.queryAllByTestId('item').length).toBe(2);
expect(screen.queryAllByTestId('item')[1]).toHaveTextContent('word');
});
});
|