2022 年 10 月 14 日

使用 Mocha 进行自动化测试

自动化测试将用于进一步的任务中,并且在实际项目中也广泛使用。

我们为什么要进行测试?

当我们编写一个函数时,我们通常可以想象它应该做什么:哪些参数给出哪些结果。

在开发过程中,我们可以通过运行函数并将其结果与预期结果进行比较来检查函数。例如,我们可以在控制台中执行此操作。

如果出现问题,那么我们将修复代码,再次运行,检查结果,依此类推,直到它正常工作。

但这种手动“重新运行”是不完美的。

通过手动重新运行来测试代码时,很容易遗漏一些东西。

例如,我们正在创建一个函数 f。编写了一些代码,进行测试:f(1) 正常工作,但 f(2) 不正常工作。我们修复代码,现在 f(2) 正常工作了。看起来完整了吗?但我们忘记重新测试 f(1) 了。这可能会导致错误。

这是很典型的。当我们开发一些东西时,我们会在头脑中保留很多可能的用例。但是很难期望程序员在每次更改后手动检查所有这些用例。因此,很容易修复一件事而破坏另一件事。

自动化测试意味着除了代码之外,还会单独编写测试。它们以各种方式运行我们的函数,并将结果与预期结果进行比较。

行为驱动开发 (BDD)

让我们从一种名为 行为驱动开发 或简称 BDD 的技术开始。

BDD 三者合一:测试、文档和示例。

为了理解 BDD,我们将研究一个实际的开发案例。

开发“pow”:规范

假设我们想要创建一个函数 pow(x, n),它将 x 提升到整数幂 n。我们假设 n≥0

该任务只是一个示例:JavaScript 中有 ** 运算符可以执行此操作,但这里我们专注于可以应用于更复杂任务的开发流程。

在创建 pow 的代码之前,我们可以想象该函数应该做什么并对其进行描述。

这样的描述称为规范,简称规范,其中包含用例的描述以及它们的测试,如下所示

describe("pow", function() {

  it("raises to n-th power", function() {
    assert.equal(pow(2, 3), 8);
  });

});

规范有三个主要的组成部分,您可以在上面看到

describe("title", function() { ... })

我们描述的是什么功能?在我们的例子中,我们描述的是函数 pow。用于对“工作者”进行分组 - it 块。

it("use case description", function() { ... })

it 的标题中,我们以人类可读的方式描述特定用例,而第二个参数是测试它的函数。

assert.equal(value1, value2)

如果实现正确,it 块内的代码应该在没有错误的情况下执行。

函数 assert.* 用于检查 pow 是否按预期工作。我们在这里使用其中一个 - assert.equal,它比较参数,如果不相等则产生错误。这里它检查 pow(2, 3) 的结果是否等于 8。还有其他类型的比较和检查,我们稍后会添加。

该规范可以执行,它将运行在it块中指定的测试。我们稍后会看到。

开发流程

开发流程通常如下所示

  1. 编写初始规范,其中包含最基本功能的测试。
  2. 创建初始实现。
  3. 为了检查它是否有效,我们运行测试框架Mocha(稍后会详细介绍),它运行规范。虽然该功能尚未完成,但会显示错误。我们进行更正,直到一切正常为止。
  4. 现在,我们有了带有测试的工作初始实现。
  5. 我们向规范中添加更多用例,这些用例可能尚未得到实现的支持。测试开始失败。
  6. 转到 3,更新实现,直到测试没有错误为止。
  7. 重复步骤 3-6,直到功能就绪。

因此,开发是迭代的。我们编写规范,实现它,确保测试通过,然后编写更多测试,确保它们有效等。最后,我们既有了工作实现,也有了针对它的测试。

让我们在实际案例中了解此开发流程。

第一步已经完成:我们为pow制定了初始规范。现在,在进行实现之前,让我们使用一些 JavaScript 库来运行测试,只是为了查看它们是否有效(它们都将失败)。

规范的实际应用

在本教程中,我们将使用以下 JavaScript 库进行测试

  • Mocha – 核心框架:它提供常见的测试函数,包括describeit以及运行测试的主函数。
  • Chai – 具有许多断言的库。它允许使用许多不同的断言,现在我们只需要assert.equal
  • Sinon – 一个用于监视函数、模拟内置函数等的库,我们稍后会需要它。

这些库适用于浏览器内和服务器端测试。这里我们将考虑浏览器变体。

包含这些框架和pow规范的完整 HTML 页面

<!DOCTYPE html>
<html>
<head>
  <!-- add mocha css, to show results -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
  <!-- add mocha framework code -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
  <script>
    mocha.setup('bdd'); // minimal setup
  </script>
  <!-- add chai -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
  <script>
    // chai has a lot of stuff, let's make assert global
    let assert = chai.assert;
  </script>
</head>

<body>

  <script>
    function pow(x, n) {
      /* function code is to be written, empty now */
    }
  </script>

  <!-- the script with tests (describe, it...) -->
  <script src="test.js"></script>

  <!-- the element with id="mocha" will contain test results -->
  <div id="mocha"></div>

  <!-- run tests! -->
  <script>
    mocha.run();
  </script>
</body>

</html>

该页面可以分为五个部分

  1. <head> – 为测试添加第三方库和样式。
  2. 包含要测试的函数的<script>,在我们的案例中 – 包含pow的代码。
  3. 测试 – 在我们的案例中,外部脚本test.js具有上述describe("pow", ...)
  4. HTML 元素<div id="mocha">将由 Mocha 用于输出结果。
  5. 测试通过命令 mocha.run() 启动。

结果

现在,测试失败了,出现了错误。这是合乎逻辑的:我们在 pow 中有一个空函数代码,所以 pow(2,3) 返回 undefined 而不是 8

对于未来,让我们注意,还有更高级别的测试运行器,比如 karma 和其他,它们可以轻松地自动运行许多不同的测试。

初始实现

让我们对 pow 进行一个简单的实现,以便通过测试

function pow(x, n) {
  return 8; // :) we cheat!
}

哇,现在它可以工作了!

改进规范

我们所做的绝对是一种作弊。该函数不起作用:尝试计算 pow(3,4) 会得到不正确的结果,但测试通过了。

…但这种情况很典型,在实践中会发生。测试通过,但函数工作错误。我们的规范是不完美的。我们需要向其中添加更多用例。

让我们添加另一个测试来检查 pow(3, 4) = 81

我们可以选择两种方法来组织这里的测试

  1. 第一个变体——在同一个 it 中添加一个 assert

    describe("pow", function() {
    
      it("raises to n-th power", function() {
        assert.equal(pow(2, 3), 8);
        assert.equal(pow(3, 4), 81);
      });
    
    });
  2. 第二个变体——进行两个测试

    describe("pow", function() {
    
      it("2 raised to power 3 is 8", function() {
        assert.equal(pow(2, 3), 8);
      });
    
      it("3 raised to power 4 is 81", function() {
        assert.equal(pow(3, 4), 81);
      });
    
    });

主要区别在于当 assert 触发错误时,it 块会立即终止。因此,在第一个变体中,如果第一个 assert 失败,那么我们永远不会看到第二个 assert 的结果。

将测试分开很有用,可以获取有关正在发生的事情的更多信息,因此第二个变体更好。

除此之外,还有一条规则值得遵循。

一个测试检查一件事。

如果我们查看测试并看到其中有两个独立的检查,最好将其拆分为两个更简单的检查。

因此,让我们继续第二个变体。

结果

正如我们所料,第二个测试失败了。当然,我们的函数总是返回 8,而 assert 则期望 81

改进实现

让我们编写更真实的东西以通过测试

function pow(x, n) {
  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}

为了确保函数正常工作,让我们对更多值进行测试。我们可以使用 for 生成 it 块,而不是手动编写它们

describe("pow", function() {

  function makeTest(x) {
    let expected = x * x * x;
    it(`${x} in the power 3 is ${expected}`, function() {
      assert.equal(pow(x, 3), expected);
    });
  }

  for (let x = 1; x <= 5; x++) {
    makeTest(x);
  }

});

结果

嵌套描述

我们将添加更多测试。但在那之前,让我们注意辅助函数 makeTestfor 应该组合在一起。我们在其他测试中不需要 makeTest,它只在 for 中需要:它们的共同任务是检查 pow 如何提升到给定的幂。

分组使用嵌套 describe 完成

describe("pow", function() {

  describe("raises x to power 3", function() {

    function makeTest(x) {
      let expected = x * x * x;
      it(`${x} in the power 3 is ${expected}`, function() {
        assert.equal(pow(x, 3), expected);
      });
    }

    for (let x = 1; x <= 5; x++) {
      makeTest(x);
    }

  });

  // ... more tests to follow here, both describe and it can be added
});

嵌套的 describe 定义了一个新的测试“子组”。在输出中,我们可以看到标题缩进

将来,我们可以在顶级添加更多 itdescribe,并使用它们自己的辅助函数,它们不会看到 makeTest

before/afterbeforeEach/afterEach

我们可以设置 before/after 函数,它们在运行测试之前/之后执行,还可以设置 beforeEach/afterEach 函数,它们在每个 it 之前/之后执行。

例如

describe("test", function() {

  before(() => alert("Testing started – before all tests"));
  after(() => alert("Testing finished – after all tests"));

  beforeEach(() => alert("Before a test – enter a test"));
  afterEach(() => alert("After a test – exit a test"));

  it('test 1', () => alert(1));
  it('test 2', () => alert(2));

});

运行顺序将是

Testing started – before all tests (before)
Before a test – enter a test (beforeEach)
1
After a test – exit a test   (afterEach)
Before a test – enter a test (beforeEach)
2
After a test – exit a test   (afterEach)
Testing finished – after all tests (after)
在沙箱中打开示例。

通常,beforeEach/afterEachbefore/after 用于在测试(或测试组)之间执行初始化、清零计数器或执行其他操作。

扩展规范

pow 的基本功能已完成。开发的第一次迭代已完成。当我们庆祝并喝香槟时 - 让我们继续改进它。

如前所述,函数 pow(x, n) 旨在使用正整数 n

为了指示数学错误,JavaScript 函数通常返回 NaN。让我们对 n 的无效值执行相同的操作。

让我们首先将行为添加到规范中(!)

describe("pow", function() {

  // ...

  it("for negative n the result is NaN", function() {
    assert.isNaN(pow(2, -1));
  });

  it("for non-integer n the result is NaN", function() {
    assert.isNaN(pow(2, 1.5));
  });

});

包含新测试的结果

新添加的测试失败,因为我们的实现不支持它们。BDD 就是这样完成的:首先,我们编写失败的测试,然后为它们实现。

其他断言

请注意断言 assert.isNaN:它检查 NaN

Chai 中还有其他断言,例如

  • assert.equal(value1, value2) – 检查相等性 value1 == value2
  • assert.strictEqual(value1, value2) – 检查严格相等性 value1 === value2
  • assert.notEqualassert.notStrictEqual – 对上述内容进行反向检查。
  • assert.isTrue(value) – 检查 value === true
  • assert.isFalse(value) – 检查 value === false
  • …完整列表在 文档

因此,我们应该向 pow 添加几行

function pow(x, n) {
  if (n < 0) return NaN;
  if (Math.round(n) != n) return NaN;

  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}

现在它可以工作了,所有测试都通过

在沙箱中打开完整的最终示例。

摘要

在 BDD 中,规范优先,然后是实现。最后,我们同时拥有规范和代码。

规范可以用三种方式使用

  1. 作为测试 – 它们保证代码正常工作。
  2. 作为文档describeit 的标题说明了函数的作用。
  3. 作为示例 – 测试实际上是工作示例,展示了如何使用函数。

有了规范,我们可以安全地改进、更改,甚至从头重写函数,并确保它仍然正常工作。

这在大型项目中尤其重要,因为函数在许多地方使用。当我们更改此类函数时,根本无法手动检查使用它的每个地方是否仍然正常工作。

如果没有测试,人们有两种方法

  1. 无论如何执行更改。然后我们的用户遇到错误,因为我们可能无法手动检查某些内容。
  2. 或者,如果错误的惩罚很严厉,因为没有测试,人们就会害怕修改此类函数,然后代码就会过时,没有人愿意参与其中。不利于开发。

自动化测试有助于避免这些问题!

如果项目包含测试,则根本不存在此类问题。在进行任何更改后,我们可以运行测试,并在几秒钟内看到很多检查结果。

此外,经过良好测试的代码具有更好的架构。

自然,这是因为自动测试的代码更容易修改和改进。但还有另一个原因。

要编写测试,代码应按以下方式组织:每个函数都有一个明确描述的任务、定义明确的输入和输出。这意味着从一开始就有一个良好的架构。

在现实生活中,有时并不那么容易。有时在编写实际代码之前很难编写规范,因为尚不清楚它应该如何表现。但总体而言,编写测试可以使开发更快、更稳定。

在教程的后面,您将遇到许多包含内置测试的任务。因此,您将看到更多实际示例。

编写测试需要良好的 JavaScript 知识。但我们才刚刚开始学习它。因此,为了解决所有问题,从现在开始,您不必编写测试,但您应该已经能够阅读它们,即使它们比本章中的内容复杂一些。

任务

重要性:5

下面 pow 的测试有什么问题?

it("Raises x to the power n", function() {
  let x = 5;

  let result = x;
  assert.equal(pow(x, 1), result);

  result *= x;
  assert.equal(pow(x, 2), result);

  result *= x;
  assert.equal(pow(x, 3), result);
});

P.S. 语法上测试是正确的,并且通过。

该测试展示了开发人员在编写测试时遇到的诱惑之一。

我们这里实际上有 3 个测试,但以一个包含 3 个断言的单个函数的形式布局。

有时这样写会更容易,但如果发生错误,则很难看出出了什么问题。

如果错误发生在复杂执行流的中间,那么我们必须找出该点的数据。我们实际上必须调试测试

最好将测试分解为多个 it 块,并用清晰的输入和输出编写。

像这样

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  it("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});

我们用 describe 和一组 it 块替换了单个 it。现在,如果出现故障,我们将清楚地看到数据是什么。

我们还可以隔离单个测试,并通过编写 it.only(而不是 it)在独立模式下运行它

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  // Mocha will run only this block
  it.only("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});
教程地图

评论

评论前请阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将其包装在 <pre> 标记中,对于超过 10 行 - 使用沙箱 (plnkrjsbincodepen…)