独立开发和测试 React 组件
编写前端代码很容易,但编写可靠的前端代码就难了。
现代用户界面比以往任何时候都更加复杂。因此,对自己的代码充满信心至关重要。将用户界面组件隔离开发和测试,就能帮助你实现这一点。
本文将介绍如何为 React 构建一个独立、可靠的图像组件。我们将使用Storybook、Testing Library和Jest等工具。✨
如需跟着操作,请使用此仓库作为模板。
安装依赖项后,运行命令yarn storybook启动 Storybook,运行命令yarn test --watch启动测试运行器。
我们先从测试开始。
在开始开发组件之前编写测试用例至关重要。我们希望组件能够满足测试用例的要求,而不是反过来。测试可以在开发过程中提供持续的反馈。一旦所有测试都通过,就可以确保代码能够按预期运行。
不过,有一点你必须牢记。由于代码质量是通过你编写的测试来验证的,因此编写好的测试至关重要。一个好的单元测试应该易读、快速、可靠,并且能够覆盖组件的所有不同使用场景。
现在我们来编写测试用例。
// src/components/Image/Image.test.tsx
import * as React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { Image } from './';
import { photoSrc } from '../../constants';
const altText = 'abcd';
describe('Image', () => {
it('should render the image properly', async () => {
// render the Image component
const { getByAltText } = render(<Image src={photoSrc} alt={altText} />);
// retrieve a reference to the image
const image = getByAltText(altText) as HTMLImageElement;
// load the image
fireEvent.load(image);
// verify that the image exists on the DOM
expect(image).toBeTruthy();
// verify the src of the image
expect(image.src).toEqual(photoSrc);
});
});
测试当然不会通过。我们还没有编写组件的标记代码。现在就来编写吧。
// src/components/Image/index.tsx
import React from 'react';
// import { fallbackSrc, loadingSrc } from '../../constants';
export interface ImageProps {
src: string;
alt: string;
height?: string | number;
}
export const Image: React.FC<ImageProps> = ({ src, alt, height = '400px' }) => {
return (
<>
<img
src={src}
alt={alt}
style={{ height, width: 'auto', borderRadius: '10px' }}
/>
</>
);
};
你会发现测试现在通过了。✔️太棒了!(如果出现错误,请重启测试运行程序。)
等等……但是我们的组件长什么样呢?我们应该把它渲染到 React 应用的首页路由上吗?🤔
不,我们会使用 Storybook 来实现这个功能。
让我们为组件编写故事。
Storybook 记录了组件的渲染状态。我们为每个组件编写多个 Storybook,分别描述组件可以支持的不同状态。Storybook 允许我们独立开发 React 组件。如果您还不熟悉 Storybook,我强烈建议您阅读此页面。
// src/components/Image.stories.tsx
import React from 'react';
import { Story, Meta } from '@storybook/react';
import { Image, ImageProps } from './';
import { photoSrc } from '../../constants';
export default {
title: 'Example/Image',
component: Image,
argTypes: {
src: { control: 'text' },
alt: { control: 'text' }
}
} as Meta;
const Template: Story<ImageProps> = args => <Image {...args} />;
export const Primary = Template.bind({});
Primary.args = {
src: photoSrc,
alt: 'Sample alt text'
};
好了!我们的图像组件看起来简洁多了。但是目前它对加载状态和错误的处理还不完善。让我们为这两种情况编写测试。请将你的测试文件代码替换为以下内容:
// src/Image/Image.test.tsx
import * as React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { Image } from './';
import { fallbackSrc, loadingSrc, photoSrc } from '../../constants';
const altText = 'abcd';
describe('Image', () => {
it('should render the image properly', async () => {
// render the Image component
const { getByAltText } = render(<Image src={photoSrc} alt={altText} />);
// retrieve a reference to the image
const image = getByAltText(altText) as HTMLImageElement;
// load the image
fireEvent.load(image);
// verify that the image exists on the DOM
expect(image).toBeTruthy();
// verify the src of the image
expect(image.src).toEqual(photoSrc);
});
it('should display the loader until the image loads', async () => {
const { getByAltText } = render(<Image src={photoSrc} alt={altText} />);
const image = getByAltText(altText) as HTMLImageElement;
// verify that the src of the image matches the loader. note that the image has not been loaded yet.
expect(image.src).toEqual(loadingSrc);
});
it('should handle errors and render the fallback', async () => {
const { getByAltText } = render(<Image src="#" alt={altText} />);
const image = getByAltText(altText) as HTMLImageElement;
// fire the error event for the image
fireEvent.error(image);
// verify that the src of the image matches our fallback
expect(image.src).toEqual(fallbackSrc);
});
// an extra test case that verifies that our height prop behaves as expected
it('should apply the provided height', async () => {
const height = '200px';
const { getByAltText } = render(
<Image src={photoSrc} alt={altText} height={height} />
);
const image = getByAltText(altText) as HTMLImageElement;
fireEvent.load(image);
expect(image.style.height).toEqual(height);
});
});
我们还为该属性添加了一个额外的测试用例height。多一些(好的😉)测试用例总是有益无害的!
新增的三个测试中,有两个确实会失败。让我们重新审视一下组件的代码,并进行修改,使测试能够通过。请将组件的源代码修改为与以下内容一致:
// src/components/Image/index.tsx
import React from 'react';
import { fallbackSrc, loadingSrc } from '../../constants';
export interface ImageProps {
src: string;
alt: string;
height?: string | number;
}
export const Image: React.FC<ImageProps> = ({ src, alt, height = '400px' }) => {
// whether an error has occured or not
const [err, setErr] = React.useState(false);
// whether the image is loading or not
const [loading, setLoading] = React.useState(true);
return (
<>
<img
// use the fallback image as src if an error has occured
// use the loader image as src if the image is still loading
src={!err ? (loading ? loadingSrc : src) : fallbackSrc}
alt={alt}
style={{ height, width: 'auto', borderRadius: '10px' }}
// set loading to false once the image has finished loading
onLoad={() => setLoading(false)}
// set err to true if an error occurs
onError={() => setErr(true)}
/>
</>
);
};
这段代码乍一看可能令人望而生畏,但其实并不复杂。我们使用状态来跟踪两件事:图片是否仍在加载,以及是否发生了错误。然后,我们利用这些状态变量,根据条件渲染带有相应 src 属性的图片。就这么简单!
现在让我们编写一个 Story 来查看备用图片的实际效果。将以下代码添加到组件的 .stories 文件末尾。
export const Src404 = Template.bind({});
Src404.args = {
src: '#',
alt: 'something broke'
};
就这样,我们为组件添加了一个新的故事(一个有点悲伤的故事)。您可以轻松切换组件的状态,查看组件的运行情况。这就是 Storybook 的强大之处!
你还会注意到,所有测试用例现在都通过了!🥳 这些绿色的勾号是不是很可爱?
就这样,我们已经成功地独立开发出了一个可靠的React 组件。
你一定对刚刚写的代码很有信心吧?感觉是不是很棒?🤩
😇 如果这篇文章对你有帮助,请在推特上关注我。我保证不会让你失望。
💡 如果您对此感兴趣,请访问componentdriven.org了解更多关于组件驱动开发流程的信息。
文章来源:https://dev.to/dhaiwat10/develop-test-react-components-in-isolation-3714