Pr篇-关于antdweb3单测覆盖那些事儿

189 阅读4分钟

Hello,大家好👋,我是 thinkasany,今年一月陆续蹭了多个单测覆盖的pr之后,今天终于覆盖率达到100%了,准备分享一下我在Ant Design Web3 写单测的一些经验和感悟。

img_v3_027f_228a8d43-6270-4819-b2ec-11b821ca951g.jpg

image.png

「为什么需要单元测试」

通过编写和运行单元测试,开发者能够快速验证代码的各个部分是否按照预期工作,有利于保证系统功能的正确可用。

此外,单元测试还有很多好处,比如:

  1. 改进代码:编写单元测试的过程中,开发者能够再次审视业务流程和功能的实现,更容易发现一些代码上的问题。

  2. 利于重构:如果已经编写了一套可自动执行的单元测试代码,那么每次修改代码或重构后,只需要再自动执行一遍单元测试,就知道修改是否正确了,能够大幅提高效率和项目稳定性。

  3. 文档沉淀:编写详细的单元测试本身也可以作为一种文档,说明代码的预期行为。

1. 改进代码

比方说补充测试用例的时候,就会mock各种情况去覆盖,mock之前肯定需要理解具体逻辑,这个时候就可以再次审视代码是否合理。当时检查的时候就发现存在一些不合理的deadcode,是执行不到的,就做了优化代码的pr。

image.png

2. 利于重构

我们可以大大方方的接受各种pr,一旦出现break change的逻辑,ci运行就会failure,我们就可以去检查具体哪儿行导致的问题,减轻了人工去考虑逻辑合理性的工作,并且提高了稳定性。

image.png

当feature代码的覆盖率降低的时候,ci也会报错以提示我们去完善覆盖。

image.png

3. 文档沉淀

可以通过单测代码去理解预期行为,比如这边可以看连接链、钱包选择..

image.png

「如何检查覆盖率」

  1. 运行命令
pnpm run test:coverage
  1. 打开index.html image.png
  2. 检查全部覆盖情况,或者可以点击进去查看具体组件的覆盖情况。

image.png

「场景分析」

一、渲染

为了贴近浏览器现实场景, 选用 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
    });

image.png

二、交互 & 事件

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。 image.png