前情回顾
在上一篇中我就说过,之前采用的是先进行需求分析,功能使用,代码开发,测试添加的方式完成一个组件。这种方式在编写测试用例时让我感觉到一种不舒服的感觉,因为它是根据已经开发出来的组件进行编写的,所以需要不停的看已经开发出来的组件代码,根据组件代码来决定需要写什么样的测试用例,那么这样编写出的测试用例很有可能就是错误的,导致测试通过。 那么我从这一节开始,将使用TDD的开发模式进行开发。
moon-ui 仅用于学习使用,组件功能参考antd,内部代码尽量由我个人编写实现,部分代码内容可能会参考antd的实现。
一、需求分析
第一步仍然是需求分析,借鉴一下 Antd 的功能来进行分析,一个 Alert 组件具备以下功能:
- 首先 Alert 拥有着
message属性用于展示需要展示内容 - Alert组件跟Button组件一样,有不同的
type,这些type会展示不同的背景颜色 - 还有左边的
icon及 控制是否展示的showIcon两个属性,展示的图标根据 type 类型决定 - 当存在
description时,message字体增大,同时icon也会变为大号的 - 有控制是否现实关闭按钮的
closable属性,并且支持自定义按钮的closeIcon属性,以及点击关闭的回调事件onClose banner用于顶部展示公告,并且默认type = "warning",showIcon = true
二、TDD 第一步
在src/components下创建Alert及文件,当前整体项目目录如下
moon-ui
├─ package.json
├─ src
│ ├─ App.css
│ ├─ App.tsx
│ ├─ components
│ │ ├─ Alert
│ │ │ ├─ __test__
│ │ │ │ └─ alert.test.tsx
│ │ │ └─ alert.tsx
│ │ └─ Button
│ │ ├─ __test__
│ │ │ └─ button.test.tsx
│ │ ├─ _style.scss
│ │ ├─ button.tsx
│ ├─ icons
│ │ ├─ _index.scss
│ │ └─ loadingOutlined
│ │ ├─ _style.scss
│ │ └─ index.tsx
│ ├─ index.css
│ ├─ index.tsx
│ ├─ logo.svg
│ ├─ react-app-env.d.ts
│ ├─ reportWebVitals.ts
│ ├─ setupTests.ts
│ └─ styles
│ ├─ _mixin.scss
│ ├─ _reboot.scss
│ ├─ _variables.scss
│ └─ index.scss
├─ tsconfig.json
└─ yarn.lock
alert.tsx文件内容如下
// src/components/Alert/alert.tsx
const Alert = () => {
return (
<div>
</div>
)
}
export default Alert
TDD 的开发方式就是先写测试用例,再根据测试结果写实际的运行代码,先在alert.test.tsx中写上一个渲染最基本的Alert组件,并给它传入message属性。
// src/components/Alert/__test__/alert.test.tsx
import { render } from '@testing-library/react'
import Alert from '../alert'
describe('test Alert component', () => {
it('should render the correct default alert', () => {
const wrapper = render(<Alert />)
// 一般应用在想要测试的元素上 getByTestId 方法是 jest 提供的
// 需要在对应元素上有 data-testid
const element = wrapper.getByTestId('test-alert')
})
})
在运行yarn test后就会出现 ❌ 错误提示TestingLibraryElementError: Unable to find an element by: [data-testid="test-alert"]
意思就是没有在元素中找到有属性 data-testid且值为test-alert的元素。此时只需要在Alert组件上加上这个即可
// src/components/Alert/alert.tsx
const Alert = () => {
return (
<div data-testid="test-alert">
</div>
)
}
export default Alert
再看控制台就会发现测试通过 ✅ 。
三、添加 message
其中,在写上message属性时,编辑器就会提醒message不存在Alert组件上,可以此时就在Alert组件上定义props的类型 包含着message的接口,其中message是ReactNode类型
// src/components/Alert/alert.tsx
interface AlertProps {
message: React.ReactNode
}
const Alert = (props: AlertProps) => {
return (
<div data-testid="test-alert">
</div>
)
}
export default Alert
测试通过 ✅
接着来看一下 message 的渲染,在alert.test.tsx中添加上message并尝试测试其渲染
// src/components/Alert/__test__/alert.test.tsx
// ......
it('should render the correct default alert', () => {
const message = "i am a message"
// 添加上 message
const wrapper = render(<Alert message={message}/>)
const element = wrapper.getByTestId('test-alert')
// 并尝试通过 message 找到该元素
const textElment = wrapper.getByText(message)
expect(element).toBeInTheDocument()
expect(textElment).toBeInTheDocument()
})
// ......
测试抛错❌ ,message没有被渲染上,在Alert组件中加上message的渲染即可。
四、类型 type
类型有4种:success、info、warning、error
在测试函数中新写一个测试块,使用上type属性,其值为AlertType.Success,此时组件上没有type并且也没有地方能找到AlertType这个变量。那么就需要在Alert组件上添加type属性,并在其中声明AlertType。
// src/components/Alert/__test__/alert.test.tsx
// ......
it('should render the correct type on different props', () => {
const wrapper = render(<Alert message="msg" type={AlertType.Success} />)
})
// ......
Alert 组件:
// src/components/Alert/alert.tsx
export enum AlertType {
Success = 'success',
Info = 'info',
Warning = 'warning',
Error = 'error'
}
interface AlertProps {
message: React.ReactNode,
type?: AlertType
}
function Alert(props: AlertProps) {
const { message } = props
return (
<div data-testid="test-alert">
<div>{message}</div>
</div>
)
}
export default Alert
测试通过 ✅
不同的类型有不同的样式,也就是需要根据类型type去添加对应的className,这与上一章写Button组件一致,但这里的思路是先测试,再根据测试报错来添加业务代码。
// src/components/Alert/__test__/alert.test.tsx
const TEST_ID = 'test-alert'
// ......
it('should render the correct type on different props', () => {
const wrapper = render(<Alert message="success" type={AlertType.Success} />)
// 根据 id 获取元素
const element = wrapper.getByTestId(TEST_ID)
// 断言 元素 应该拥有的 class
expect(element).toHaveClass('alert alert-success-wrapper')
})
// ......
此时因为组件代码中并不含有alert alert-success-wrapper这两个class 报错
在
Alert组件中添加上对应的 class 即可
// src/components/Alert/alert.tsx
export enum AlertType {
Success = 'success',
Info = 'info',
Warning = 'warning',
Error = 'error'
}
interface AlertProps {
message: React.ReactNode,
type?: AlertType
}
function Alert(props: AlertProps) {
const { message } = props
const baseClassName = `alert-${type}`
const classes = classNames('alert', {
[`${baseClassName}-wrapper`]: type
})
return (
<div className={classes} data-testid="test-alert">
<div>{message}</div>
</div>
)
}
export default Alert
保存以后测试通过 ✅ ,接着只需要在测试文件中<Alert />组件上更换剩下的几种type来测试一下看其他类型是否成功加上对应的class,然后在_style.scss 文件中添加上几种样式(这里省略了封装_mixin.scss中的alert-style函数的描述,其实就是设置background和border-color)。
// src/components/Alert/_style.scss
.alert {
box-sizing: border-box;
border: 1px solid;
position: relative;
padding: 8px 15px;
border-radius: 2px;
}
.alert-success-wrapper {
@include alert-style($success-color-deprecated-bg, $success-color-deprecated-border)
}
.alert-info-wrapper {
@include alert-style($info-color-deprecated-bg, $info-color-deprecated-border)
}
.alert-warning-wrapper {
@include alert-style($warning-color-deprecated-bg, $warning-color-deprecated-border)
}
.alert-error-wrapper {
@include alert-style($error-color-deprecated-bg, $error-color-deprecated-border)
}
五、支持关闭
如下图实现点击按钮关闭Alert的功能
编写测试用例如下:
//
import userEvent from '@testing-library/user-event'
it('could be closed', () => {
const onClose = jest.fn() // 模拟一个事件
const wrapper = render(
<Alert
message="closabel"
closable // 设置为可关闭
onClose={onClose} // 关闭回调
/>
)
const element = wrapper.getByTestId(TEST_ID)
const closeElement = wrapper.getByTestId(TEST_CLOSE_ICON_ID)
userEvent.click(closeElement) // userEvent 与 fireEvent 一样用于事件触发
expect(onClose).toHaveBeenCalled() // 判断是否调用了事件
expect(element).not.toBeInTheDocument() // 接着判断元素是否不在页面上
})
it('support closeIcon', () => {
// 测试是否支持自定义关闭元素
const wrapper = render(<Alert closable closeIcon={<span>close</span>} message="" />)
expect(wrapper.getByText('close')).toBeInTheDocument()
})
根据测试用例的失败提示编写代码
// src/components/Alert/alert.tsx
export enum AlertType {
Success = 'success',
Info = 'info',
Warning = 'warning',
Error = 'error'
}
interface AlertProps {
message: React.ReactNode,
type?: AlertType
}
function Alert(props: AlertProps) {
const { message } = props
const baseClassName = `alert-${type}`
const classes = classNames('alert', {
[`${baseClassName}-wrapper`]: type
})
{/* 是否关闭 */}
const [closed, setClosed] = useState(false)
{/* 关闭事件 */}
const handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
setClosed(true)
props.onClose?.(e)
}
{/* 判断为关闭则设置为返回 null */}
if (closed) return null
return (
<div className={classes} data-testid="test-alert">
<div>{message}</div>
{/* 添加关闭元素 */}
{
closable && (
<button
type="button"
data-testid="test-alert-close-icon"
className="alert-close-icon"
onClick={handleClose}
>
<span className="alert-close-text">{closeIcon}</span>
</button>
)
}
</div>
)
}
export default Alert
接着添加一下按钮的样式就可以了,样式代码就不贴了,github 地址在第一篇。
结尾
尽管我还没有给它加上 icon,但我想写到这里就结尾好了,老实讲,在一边写组件一边写文章的过程真的是比较折磨的一件事,这个系列我会争取写下去,后续在做的过程中会保持以TDD的方式去开发,但是在文章输出时不会涉及到了,主要是写起文章来比较麻烦。
最近会开一个算法系列,把常用的数据结构和算法都写上一遍,算是写这个系列的一点调味品吧(因为真的有点儿枯燥~)。