编写集成测试在单位测试框架内运行,例如开玩笑
#javascript #cypress #测试 #jest

什么是集成测试,为什么重要?

集成测试是一种软件测试,将代码库的各个单元组合在一起,并将其作为一个组进行测试。这种类型的测试基本上是为了在集成单元之间的相互作用中暴露故障。

它们很重要,因为:

  • 他们暴露了在单元测试过程中可能并不明显的接口问题。这是诸如系统不同部分之间的功能之间传递的错误数据类型或值。

  • 他们可以验证系统要求,确保整体产品准备就绪。

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

这类测试的伟大之处在于,它们使您对您的应用程序按预期运行的运作非常高度信心。您有效地将一个机器人点击在您的应用程序周围,并在每次合并中给您大拇指。很棒。

但是...这是有代价的!

E2E测试有一些主要问题。

  1. 它们常常片状且难以维护。
  2. 他们总是很慢。
  3. 他们(更)难以调试。

这在实践中的意思是,我们没有编写足够的这些类型的测试,通常仅仅遵循用户通过应用程序所采取的快乐道路。我们将对关键行为充满信心,但是通常会有一系列未经测试的功能。

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

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

单元测试形成金字塔的底部。这些都是众多,快速且便宜的运行,它们的辩论性(是否存在?)很棒。但是他们给您的信心对您的应用程序实际上按预期工作是最小的。

顶部的

e2e给出了最高的信心,但覆盖范围较少(除非您想等待30分钟以上的CI)和维护成本最高。

那么集成测试呢?在网络中,我们经常忽略这些全部。毕竟,如何在不在浏览器(E2E)中运行它作为完整系统测试Web应用程序?现实是,我们通常会运行更多的变形沙漏形状,例如:

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

好吧,这是一个非常糟糕的表示,但是您明白了。我们有一个宽大的空隙,需要充满:

  1. 快速运行。
  2. 给出了很高的信心(尽管不如E2E高)。
  3. 很容易辩论。

没有UI,我们如何实现集成测试?

我们可以以无头的方式构建应用程序,该应用程序本身可以在不需要向DOM渲染任何内容的情况下工作。这就是我对the Pivot framework所做的。创建一个应用程序而不锚定在DOM元素的情况下:

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

我在这里没有时空来介绍枢轴应用程序的所有细节,但是它的要旨是,包括路由(至关重要的)是国家管理的一部分,因此应用程序可以简单地通过旋转商店,发射操作并测试状态来运行。

我将在未来的文章中深入研究自己的枢轴,但是就目前而言,让我们看看这对我们的测试意味着什么。以下是集成测试的示例。它具有富含仪式,而不是在柏树中运行,并且不会测试任何DOM元素的状态。相反,它测试了应用程序的内部状态。

这意味着,是的,我们对E2E测试的信心较少,但是与单位测试相比,它仍然给我们带来更多的 置信度。它充满了海湾。更重要的是,这些类型的集成测试几乎与单位测试一样快,并给出相同级别的调试性 - 即从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);
}

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

此测试仅需几毫秒即可运行。

但是它给我们带来了什么信心?好吧,我们知道登录系统在表面上工作,并且我们知道路由器正在聆听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');
});

太好了!现在,我们知道身材系统确实有效。现在我们也知道我们可以使用内部router API导航。

结论

我认为这种测试有点好处,因为它使我们对应用程序的业务逻辑有效,而且写得很快,以至于我们可以真正扩展有意义的测试覆盖我们的应用程序。

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

通过不将应用程序的初始化耦合到UI框架中,我们将从其束缚中解放出来,并具有更大的灵活性。我们很可能会得到更清洁的代码,但这是另一个故事。

快乐测试!