Hello,大家好👋,我是 thinkasany,今年一月陆续蹭了多个单测覆盖的pr之后,今天终于覆盖率达到100%了,准备分享一下我在Ant Design Web3 写单测的一些经验和感悟。
「为什么需要单元测试」
通过编写和运行单元测试,开发者能够快速验证代码的各个部分是否按照预期工作,有利于保证系统功能的正确可用。
此外,单元测试还有很多好处,比如:
-
改进代码:编写单元测试的过程中,开发者能够再次审视业务流程和功能的实现,更容易发现一些代码上的问题。
-
利于重构:如果已经编写了一套可自动执行的单元测试代码,那么每次修改代码或重构后,只需要再自动执行一遍单元测试,就知道修改是否正确了,能够大幅提高效率和项目稳定性。
-
文档沉淀:编写详细的单元测试本身也可以作为一种文档,说明代码的预期行为。
1. 改进代码
比方说补充测试用例的时候,就会mock各种情况去覆盖,mock之前肯定需要理解具体逻辑,这个时候就可以再次审视代码是否合理。当时检查的时候就发现存在一些不合理的deadcode,是执行不到的,就做了优化代码的pr。
2. 利于重构
我们可以大大方方的接受各种pr,一旦出现break change的逻辑,ci运行就会failure,我们就可以去检查具体哪儿行导致的问题,减轻了人工去考虑逻辑合理性的工作,并且提高了稳定性。
当feature代码的覆盖率降低的时候,ci也会报错以提示我们去完善覆盖。
3. 文档沉淀
可以通过单测代码去理解预期行为,比如这边可以看连接链、钱包选择..
「如何检查覆盖率」
- 运行命令
pnpm run test:coverage
- 打开index.html
- 检查全部覆盖情况,或者可以点击进去查看具体组件的覆盖情况。
「场景分析」
一、渲染
为了贴近浏览器现实场景, 选用 mount
来进行渲染,而在 @testing-library
中对应的则是 render
方法:
import { render, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
describe('ConnectButton', () => {
it('mount correctly', () => {
expect(() => render(<ConnectButton />)).not.toThrow();
});
})
除此之外,我们还可能会遇到响应式的情况,我们可以mock一下
mockBrowser('Chrome');
it.each(['light', 'dark'] as const)(`should render in %s mode`, (theme) => {
vi.spyOn(Grid, 'useBreakpoint').mockReturnValue({
md: true, // ≥ 768px, mock PC
});
二、交互 & 事件
import { fireEvent } from '@testing-library/react';
const btn = baseElement.querySelector('.ant-web3-connect-button');
fireEvent.mouseEnter(btn); // mock 鼠标移入事件
fireEvent.click(btn!); // mock 点击事件
await vi.waitFor
是一个等待异步操作完成的表达式,通常用于测试框架中等待特定条件的满足。在测试中,可能会有一些异步操作,例如等待元素出现、等待某个状态变化等,而 waitFor
则提供了等待的机制。
我们有时候有遇到一些需要等待时间的场景,我们需要mock时间等待,比如Tooltip
组件默认 mouseEnterDelay = 0.1
。
fireEvent.mouseEnter(divElement!);
expect(onOpenChange).toHaveBeenLastCalledWith(true);
await waitFakeTimer();
我们可以封装一个waitFakeTimer
或者是直接
await new Promise((resolve) => setTimeout(resolve, 100));
/**
* Wait for a time delay. Will wait `advanceTime * times` ms.
*
* @param advanceTime Default 1000
* @param times Default 20
*/
export async function waitFakeTimer(advanceTime = 1000, times = 20) {
for (let i = 0; i < times; i += 1) {
// eslint-disable-next-line no-await-in-loop
await act(async () => {
await Promise.resolve();
if (advanceTime > 0) {
jest.advanceTimersByTime(advanceTime);
} else {
jest.runAllTimers();
}
});
}
}
三、如何 mock 模块内的方法
vi.mock('wagmi', () => {
return {
useConfig: () => {
return {};
},
// https://wagmi.sh/react/hooks/useAccount
useAccount: () => {
return {
chain: mainnet,
address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B',
connector: mockConnector,
};
},
useConnect: () => {
return {
connectors: [mockConnector],
};
},
useDisconnect: () => {
return {
disconnectAsync: () => {},
};
},
useSwitchChain: () => {
return {
switchChain: () => {},
};
},
useBalance: () => {
return {
data: {
value: 1230000000000000000,
symbol: 'WETH',
decimals: 18,
},
};
},
};
});
describe('WagmiWeb3ConfigProvider balance', () => {
it('show balance', () => {
const App = () => (
<AntDesignWeb3ConfigProvider
availableConnectors={[]}
balance
availableChains={[mainnet]}
walletFactorys={[MetaMask()]}
chainAssets={[Mainnet]}
>
<Connector>
<ConnectButton />
</Connector>
</AntDesignWeb3ConfigProvider>
);
const { baseElement } = render(<App />);
expect(baseElement.querySelector('.ant-web3-connect-button-text')?.textContent).toBe(
' 1.23 WETH',
);
expect(baseElement.querySelector('.ant-web3-icon-ethereum-filled')).toBeTruthy();
});
});
「最后」
很荣幸成为 core member,感谢愚指导、kiner-tang的帮助,希望Ant Design Web3越来越好,也希望有新的小伙伴能够加入贡献,或者从这上面学到对自己有帮助的知识。官方的课程中,我们将指导你搭建一个 DApp 的前端部分。该 DApp 将会实现一个铸造 NFT 的功能,用户连接钱包后可以点击铸造一个 NFT 并查看铸造后的 NFT。