编写在 Jest 等单元测试框架内运行的集成测试

什么是集成测试,为什么它们很重要?
集成测试是一种软件测试,其中将代码库的各个单元组合在一起并作为一个组进行测试。这种类型的测试本质上是为了暴露集成单元之间交互中的错误。

它们很重要,因为:

它们暴露了在单元测试期间可能不明显的接口问题。这是诸如函数之间传递的不正确数据类型或值或系统不同部分之间的错误通信之类的事情。

他们可以验证系统要求,确保整个产品已准备好交付。

在为网络创建的软件中,集成测试通常在浏览器或类似浏览器的环境中采用端到端测试的形式。这实际上是加载您的应用程序并通过单击和断言行为来验证功能。

这些测试的好处在于,它们让您非常有信心您的应用程序按预期工作。您实际上让机器人在您的应用程序周围点击并在每次合并时给您竖起大拇指。极好的。

然而……这是有代价的!

E2E 测试存在几个主要问题。

它们通常不稳定且难以维护。
他们总是很慢。
它们(更)难以调试。
这在实践中意味着我们没有编写足够多的此类测试,通常只是坚持用户通过应用程序所采用的快乐路径。我们会对关键行为充满信心,但通常会有大量功能未经测试。

您可能听说过测试金字塔。它可能看起来像这样:

       ^
          /   \
         / E2E \ 
        /_______\
       /         \
      /Integration\
     /_____________\
    /               \
   /   Unit Tests    \
  /___________________\

单元测试构成了金字塔的底部。它们数量众多,运行速度快且成本低廉,而且它们的可调试性(这个词存在吗?)非常棒。但是他们让您相信该应用程序确实按预期工作的信心微乎其微。

顶部的 E2E 提供了最高的置信度,但覆盖率较低(除非你想为 CI 等待 30 分钟以上),并且维护成本最高。

那么集成测试呢?在网络中,我们经常忽略这些。毕竟,如何在不在浏览器中运行的情况下将 Web 应用程序作为一个完整的系统进行测试 (E2E)?现实情况是,我们通常运行更多的是扭曲的沙漏形状,如下所示:

         ^
          /   \
         / E2E \ 
        /_______\
        \       /
         \     /
         /_____\
        /       \
       /         \
      /Unit Tests \
     /________ ____\

好吧好吧,这是一个非常糟糕的表现,但你明白了。我们有一个巨大的空白,需要用一些东西来填补:

跑得快。
给出相当高的置信度(虽然不如 E2E 高)。
易于调试。
没有 UI 怎么实现集成测试呢?
我们可以以无头的方式构建我们的应用程序,应用程序本身无需向 DOM 渲染任何内容即可运行。这就是我使用Pivot 框架所做的。创建的应用程序没有锚定到 DOM 元素,如下所示:

export const app = headless(services, slices, subscriptions);

我没有时间在这里详细介绍 Pivot 应用程序如何工作的所有细节,但要点是一切,包括路由(至关重要)都是状态管理的一部分,因此应用程序可以运行只需启动商店、触发操作和测试状态即可。

我将在以后的文章中更深入地研究 Pivot 本身,但现在,让我们看看它对我们的测试意味着什么。下面是一个集成测试的例子。它在 Vitest 中运行,而不是在 Cypress 中运行,并且不测试任何 DOM 元素的状态。相反,它测试应用程序的内部状态。

这意味着,是的,我们的信心不如端到端测试,但它确实比单元测试给了我们更多的信心。它填补了鸿沟。更重要的是,这些类型的集成测试几乎与单元测试一样快,并提供相同级别的可调试性——即从您的 IDE 中单步执行代码。

const app = headless(services, slices, subscriptions);
const project = findProjectByName('pivot');

describe('integration', () => {
  describe('router', () => {
    beforeEach(async () => {
      await app.init();
      await app.getService('router');

      const auth = await app.getService('auth');

      await auth.login('user@user.com', 'password');
    });

    it('should visit project page', async () => {
      visit(`/projects/${project.uuid}`);

      const state = await app.getSlice('router');

      expect(state.route?.name).toEqual('project');
    });
  });
});

顺便说一句,该visit实用程序通过修改历史记录并发出事件来模拟页面导航,其方式与它在浏览器中的工作方式相同popstate:

export function visit(url: string) {
  history.pushState(null, '', url);

  const popStateEvent = new PopStateEvent('popstate', {
    bubbles: true,
    cancelable: true,
    state: null,
  });

  window.dispatchEvent(popStateEvent);
}

因此,在测试中我们正在初始化应用程序和路由器,登录到应用程序,访问经过身份验证的路由,然后断言当前路由是正确的。

您可以想象这在 E2E 中的样子 - 我相信您已经做过很多次这种事情。这里的主要区别是这个测试只需要几毫秒就可以运行。

但是它给了我们什么信心呢?好吧,我们知道登录系统在表面上工作,我们知道路由器正在监听事件popstate并将我们导航到页面。我们知道允许经过身份验证的用户访问此页面的逻辑正在运行。

这已经很不错了,因为对路由器和登录系统的更改都会导致失败。

让我们添加一个测试来测试未经身份验证的用户无法访问此路由:

it('should navigate to notFound if unauthorized', async () => {
  const auth = await app.getService('auth');
  const router = await app.getService('router');

  await auth.logout();

  router.navigate({ name: 'project', params: { id: project.uuid } });

  const route = await app.waitFor(selectRoute, (route) => route?.name === 'notFound');

  expect(route?.name).toEqual('notFound');
});

伟大的!现在我们知道身份验证系统确实有效。我们现在还知道我们可以使用内部routerAPI 进行导航。

结论
我认为这种测试有点甜蜜,因为它让我们对应用程序的业务逻辑工作非常有信心,而且编写起来非常简单和快速,这意味着我们可以真正扩展有意义的测试覆盖范围应用。

当然,仍然存在 UI 测试的问题,但这并不意味着要取代任何现有的策略,只是为了增强它们。

通过不将我们的应用程序的初始化耦合到我们的 UI 框架,我们从它的束缚中解放出来并且在测试中具有更大的灵活性。更有可能的是,我们最终会得到更简洁的代码,但那是另一个故事了。

测试愉快!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q shen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值