Javascript 使用 Mocha 進(jìn)行自動(dòng)化測(cè)試

2023-02-17 10:38 更新

自動(dòng)化測(cè)試將被用于進(jìn)一步的任務(wù)中,并且還將被廣泛應(yīng)用在實(shí)際項(xiàng)目中。

我們?yōu)槭裁葱枰獪y(cè)試?

當(dāng)我們?cè)趯懸粋€(gè)函數(shù)時(shí),我們通常可以想象出它應(yīng)該做什么:哪些參數(shù)會(huì)給出哪些結(jié)果。

在開發(fā)期間,我們可以通過運(yùn)行程序來檢查它并將結(jié)果與預(yù)期進(jìn)行比較。例如,我們可以在控制臺(tái)中這么做。

如果出了問題 —— 那么我們會(huì)修復(fù)代碼,然后再一次運(yùn)行并檢查結(jié)果 —— 直到它工作為止。

但這樣的手動(dòng)“重新運(yùn)行”是不完美的。

當(dāng)通過手動(dòng)重新運(yùn)行來測(cè)試代碼時(shí),很容易漏掉一些東西。

例如,我們要?jiǎng)?chuàng)建一個(gè)函數(shù) f。寫一些代碼,然后測(cè)試:f(1) 可以執(zhí)行,但是 f(2) 不執(zhí)行。我們修復(fù)了一下代碼,現(xiàn)在 f(2) 可以執(zhí)行了??雌饋硪呀?jīng)搞定了?但是我們忘了重新測(cè)試 f(1)。這樣有可能會(huì)導(dǎo)致出現(xiàn)錯(cuò)誤。

這是非常典型的。當(dāng)我們?cè)陂_發(fā)一些東西時(shí),我們會(huì)保留很多可能需要的用例。但是不要想著程序員在每一次代碼修改后都去檢查所有的案例。所以這就很容易造成修復(fù)了一個(gè)問題卻造成另一個(gè)問題的情況。

自動(dòng)化測(cè)試意味著測(cè)試是獨(dú)立于代碼的。它們以各種方式運(yùn)行我們的函數(shù),并將結(jié)果與預(yù)期結(jié)果進(jìn)行比較。

行為驅(qū)動(dòng)開發(fā)(BDD)

我們來使用一種名為 行為驅(qū)動(dòng)開發(fā) 或簡(jiǎn)言為 BDD 的技術(shù)。

BDD 包含了三部分內(nèi)容:測(cè)試、文檔和示例。

為了理解 BDD,我們將研究一個(gè)實(shí)際的開發(fā)案例。

開發(fā) “pow”:規(guī)范

我們想要?jiǎng)?chuàng)建一個(gè)函數(shù) pow(x, n) 來計(jì)算 x 的 n 次冪(n 為整數(shù))。我們假設(shè) n≥0

這個(gè)任務(wù)只是一個(gè)例子:JavaScript 中有一個(gè) ** 運(yùn)算符可以用于冪運(yùn)算。但是在這里我們專注于可以應(yīng)用于更復(fù)雜任務(wù)的開發(fā)流程上。

在創(chuàng)建函數(shù) pow 的代碼之前,我們可以想象函數(shù)應(yīng)該做什么并且描述出來。

這樣的描述被稱作 規(guī)范(specification, spec),包含用例的描述以及針對(duì)它們的測(cè)試,如下所示:

describe("pow", function() {

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

});

正如你所看到的,一個(gè)規(guī)范包含三個(gè)主要的模塊:

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

表示我們正在描述的功能是什么。在我們的例子中我們正在描述函數(shù) pow。用于組織“工人(workers)” —— it 代碼塊。

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

it 里面的描述部分,我們以一種 易于理解 的方式描述特定的用例,第二個(gè)參數(shù)是用于對(duì)其進(jìn)行測(cè)試的函數(shù)。

?assert.equal(value1, value2)
?

it 塊中的代碼,如果實(shí)現(xiàn)是正確的,它應(yīng)該在執(zhí)行的時(shí)候不產(chǎn)生任何錯(cuò)誤。

assert.* 函數(shù)用于檢查 pow 函數(shù)是否按照預(yù)期工作。在這里我們使用了其中之一 —— assert.equal,它會(huì)對(duì)參數(shù)進(jìn)行比較,如果它們不相等則會(huì)拋出一個(gè)錯(cuò)誤。這里它檢查了 pow(2, 3) 的值是否等于 8。還有其他類型的比較和檢查,我們將在后面介紹到。

規(guī)范可以被執(zhí)行,它將運(yùn)行在 it 塊中指定的測(cè)試。我們稍后會(huì)看到。

開發(fā)流程

開發(fā)流程通??雌饋硐襁@樣:

  1. 編寫初始規(guī)范,測(cè)試最基本的功能。
  2. 創(chuàng)建一個(gè)最初始的實(shí)現(xiàn)。
  3. 檢查它是否工作,我們運(yùn)行測(cè)試框架 Mocha(很快會(huì)有更多細(xì)節(jié))來運(yùn)行測(cè)試。當(dāng)功能未完成時(shí),將顯示錯(cuò)誤。我們持續(xù)修正直到一切都能工作。
  4. 現(xiàn)在我們有一個(gè)帶有測(cè)試的能工作的初步實(shí)現(xiàn)。
  5. 我們?cè)黾痈嗟挠美揭?guī)范中,或許目前的程序?qū)崿F(xiàn)還不支持。無法通過測(cè)試。
  6. 回到第 3 步,更新程序直到測(cè)試不會(huì)拋出錯(cuò)誤。
  7. 重復(fù)第 3 步到第 6 步,直到功能完善。

如此來看,開發(fā)就是不斷地 迭代。我們寫規(guī)范,實(shí)現(xiàn)它,確保測(cè)試通過,然后寫更多的測(cè)試,確保它們工作等等。最后,我們有了一個(gè)能工作的實(shí)現(xiàn)和針對(duì)它的測(cè)試。

讓我們?cè)谖覀兊拈_發(fā)案例中看看這個(gè)開發(fā)流程吧。

在我們的案例中,第一步已經(jīng)完成了:我們有一個(gè)針對(duì) pow 的初始規(guī)范。因此讓我們來實(shí)現(xiàn)它吧。但在此之前,讓我們用一些 JavaScript 庫(kù)來運(yùn)行測(cè)試,就是看看測(cè)試是通過了還是失敗了。

行為規(guī)范

在本教程中,我們將使用以下 JavaScript 庫(kù)進(jìn)行測(cè)試:

  • Mocha —— 核心框架:提供了包括通用型測(cè)試函數(shù) ?describe ?和 ?it?,以及用于運(yùn)行測(cè)試的主函數(shù)。
  • Chai —— 提供很多斷言(assertion)支持的庫(kù)。它提供了很多不同的斷言,現(xiàn)在我們只需要用 ?assert.equal?。
  • Sinon —— 用于監(jiān)視函數(shù)、模擬內(nèi)建函數(shù)和其他函數(shù)的庫(kù),我們?cè)诤竺娌艜?huì)用到它。

這些庫(kù)都既適用于瀏覽器端,也適用于服務(wù)器端。這里我們將使用瀏覽器端的變體。

包含這些框架和 ?pow ?規(guī)范的完整的 HTML 頁面:

<!DOCTYPE html>
<html>
<head>
  <!-- add mocha css, to show results -->
  <link rel="stylesheet"  rel="external nofollow" target="_blank" >
  <!-- add mocha framework code -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js" rel="external nofollow" ></script>
  <script>
    mocha.setup('bdd'); // minimal setup
  </script>
  <!-- add chai -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js" rel="external nofollow" ></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>

該頁面可分為五個(gè)部分:

  1. ?<head>? —— 添加用于測(cè)試的第三方庫(kù)和樣式文件。
  2. ?<script>? 包含測(cè)試函數(shù),在我們的例子中 —— 和 ?pow ?相關(guān)的代碼。
  3. 測(cè)試代碼 —— 在我們的案例中是名為 ?test.js? 的腳本,它包含上面 ?describe("pow", ...)? 的那些代碼。
  4. HTML 元素 ?<div id="mocha">? 將被 Mocha 用來輸出結(jié)果。
  5. 可以使用 ?mocha.run()? 命令來開始測(cè)試。

結(jié)果:


到目前為止,測(cè)試失敗了,出現(xiàn)了一個(gè)錯(cuò)誤。這是合乎邏輯的:我們的 pow 是一個(gè)空函數(shù),因此 pow(2,3) 返回了 undefined 而不是 8。

未來,我們會(huì)注意到有更高級(jí)的測(cè)試工具,像是 karma 或其他的,使自動(dòng)運(yùn)行許多不同的測(cè)試變得更容易。

初始實(shí)現(xiàn)

為了可以通過測(cè)試,讓我們寫一個(gè) pow 的簡(jiǎn)單實(shí)現(xiàn):

function pow() {
  return 8; // :) 我們作弊啦!
}

哇哦,現(xiàn)在它可以工作了。


改進(jìn)規(guī)范

我們所做的這些絕對(duì)是作弊。函數(shù)是不起作用的:嘗試計(jì)算 pow(3,4) 的話就會(huì)得到一個(gè)不正確的結(jié)果,但是測(cè)試卻通過了。

……但是這種情況卻是在實(shí)際中相當(dāng)?shù)湫屠印y(cè)試通過了,但是函數(shù)卻是錯(cuò)誤的。我們的規(guī)范是不完善的。我們需要給它添加更多的測(cè)試用例。

這里我們又添加了一個(gè)測(cè)試來檢查 pow(3, 4) = 81。

我們可以選擇兩種方式中的任意一種來組織測(cè)試代碼:

  1. 第一種 —— 在同一個(gè) it 中再添加一個(gè) assert
  2. describe("pow", function() {
    
      it("raises to n-th power", function() {
        assert.equal(pow(2, 3), 8);
        assert.equal(pow(3, 4), 81);
      });
    
    });
  3. 第二種 —— 寫兩個(gè)測(cè)試:
  4. 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);
      });
    
    });

主要的區(qū)別是,當(dāng) assert 觸發(fā)一個(gè)錯(cuò)誤時(shí),it 代碼塊會(huì)立即終止。因此,在第一種方式中,如果第一個(gè) assert 失敗了,我們將永遠(yuǎn)不會(huì)看到第二個(gè) assert 的結(jié)果。

保持測(cè)試之間獨(dú)立,有助于我們獲知代碼中正在發(fā)生什么,因此第二種方式更好一點(diǎn)。

除此之外,還有一個(gè)規(guī)范值得遵循。

一個(gè)測(cè)試檢查一個(gè)東西。

如果我們?cè)诳礈y(cè)試代碼的時(shí)候,發(fā)現(xiàn)在其中有兩個(gè)相互獨(dú)立的檢查 —— 最好將它拆分成兩個(gè)更簡(jiǎn)單的檢查。

因此讓我們繼續(xù)使用第二種方式。

結(jié)果:


正如我們可以想到的,第二條測(cè)試失敗了。當(dāng)然啦,我們的函數(shù)總會(huì)返回 8,而 assert 期望的是 81。

改進(jìn)實(shí)現(xiàn)

讓我們寫一些更加實(shí)際的代碼來通過測(cè)試吧:

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

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

  return result;
}

為了確保函數(shù)可以很好地工作,我們來使用更多值測(cè)試它吧。除了手動(dòng)地編寫 it 代碼塊,我們可以使用 for 循環(huán)來生成它們:

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);
  }

});

結(jié)果:


嵌套描述

我們繼續(xù)添加更多的測(cè)試。但在此之前,我們需要注意到輔助函數(shù) makeTest 和 for 應(yīng)該被組合到一起。我們?cè)谄渌麥y(cè)試中不需要 makeTest,只有在 for 循環(huán)中需要它:它們共同的任務(wù)就是檢查 pow 是如何自乘至給定的冪次方。

使用嵌套的 describe 來進(jìn)行分組:

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);
    }

  });

  // ……可以在這里寫更多的測(cè)試代碼,describe 和 it 都可以添加在這。
});

嵌套的 describe 定義了一個(gè)新的 “subgroup” 測(cè)試。在輸出中我們可以看到帶有標(biāo)題的縮進(jìn):


將來,我們可以在頂級(jí)域中使用 it 和 describe 的輔助函數(shù)添加更多的 it 和 describe,它們不會(huì)看到 makeTest。

?before/after? 和 ?beforeEach/afterEach?

我們可以設(shè)置 before/after 函數(shù)來在運(yùn)行測(cè)試之前/之后執(zhí)行。也可以使用 beforeEach/afterEach 函數(shù)來設(shè)置在執(zhí)行 每一個(gè) it 之前/之后執(zhí)行。

例如:

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));

});

運(yùn)行順序?qū)椋?

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/afterEach 和 before/after 被用于執(zhí)行初始化,清零計(jì)數(shù)器或做一些介于每個(gè)測(cè)試(或測(cè)試組)之間的事情。

延伸規(guī)范

pow 的基礎(chǔ)功能已經(jīng)完成了。第一次迭代開發(fā)完成啦。當(dāng)我們慶祝和喝完香檳之后,讓我們繼續(xù)改進(jìn)它吧。

正如前面所說,函數(shù) pow(x, n) 適用于正整數(shù) n。

JavaScript 函數(shù)通常會(huì)返回 NaN 以表示一個(gè)數(shù)學(xué)錯(cuò)誤。接下來我們對(duì)無效的 n 值執(zhí)行相同的操作。

讓我們首先將這個(gè)行為加到規(guī)范中(!):

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));
  });

});

新測(cè)試的結(jié)果:


新加的測(cè)試失敗了,因?yàn)槲覀兊膶?shí)現(xiàn)方式是不支持它們的。這就是 BDD 的做法:我們首先寫一些暫時(shí)無法通過的測(cè)試,然后去實(shí)現(xiàn)它們。

Other assertions

請(qǐng)注意斷言語句 assert.isNaN:它用來檢查 NaN。

在 Chai 中也有其他的斷言,例如:

  • ?assert.equal(value1, value2)? —— 檢查相等 ?value1 == value2?。
  • ?assert.strictEqual(value1, value2)? —— 檢查嚴(yán)格相等 ?value1 === value2?。
  • ?assert.notEqual?,?assert.notStrictEqual? —— 執(zhí)行和上面相反的檢查。
  • ?assert.isTrue(value)? —— 檢查 ?value === true?。
  • ?assert.isFalse(value)? —— 檢查 ?value === false?。
  • ……完整的列表請(qǐng)見 docs

因此我們應(yīng)該給 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;
}

現(xiàn)在它可以工作了,所有的測(cè)試也都通過了:


總結(jié)

在 BDD 中,規(guī)范先行,實(shí)現(xiàn)在后。最后我們同時(shí)擁有了規(guī)范和代碼。

規(guī)范有三種使用方式:

  1. 作為 測(cè)試 —— 保證代碼正確工作。
  2. 作為 文檔 —— ?describe ?和 ?it ?的標(biāo)題告訴我們函數(shù)做了什么。
  3. 作為 案例 —— 測(cè)試實(shí)際工作的例子展示了一個(gè)函數(shù)可以被怎樣使用。

有了規(guī)范,我們可以安全地改進(jìn)、修改甚至重寫函數(shù),并確保它仍然正確地工作。

這在一個(gè)函數(shù)會(huì)被用在多個(gè)地方的大型項(xiàng)目中尤其重要。當(dāng)我們改變這樣一個(gè)函數(shù)時(shí),沒有辦法手動(dòng)檢查每個(gè)使用它們的地方是否仍舊正確。

如果沒有測(cè)試,一般有兩個(gè)辦法:

  1. 展示修改,無論修改了什么。然后我們的用戶遇到了 bug,這應(yīng)該是我們沒有手動(dòng)完成某些檢查。
  2. 如果對(duì)出錯(cuò)的懲罰比較嚴(yán)重,并且沒有測(cè)試,那么大家會(huì)很害怕修改這樣的函數(shù),然后這些代碼就會(huì)越來越陳舊,沒有人會(huì)想接觸它。這很不利于發(fā)展。

自動(dòng)化測(cè)試則有助于避免這樣的問題!

如果這個(gè)項(xiàng)目被測(cè)試代碼覆蓋了,就不會(huì)出現(xiàn)這種問題。在任何修改之后,我們都可以運(yùn)行測(cè)試,并在幾秒鐘內(nèi)看到大量的檢查。

另外,一個(gè)經(jīng)過良好測(cè)試的代碼通常都有更好的架構(gòu)。

當(dāng)然,這是因?yàn)楦采w了自動(dòng)化測(cè)試的代碼更容易修改和改進(jìn)。但還有另一個(gè)原因。

要編寫測(cè)試,代碼的組織方式應(yīng)確保每個(gè)函數(shù)都有一個(gè)清晰描述的任務(wù)、定義良好的輸入和輸出。這意味著從一開始就有一個(gè)好的架構(gòu)。

在實(shí)際開發(fā)中有時(shí)候可能并不容易,有時(shí)很難在寫實(shí)際代碼之前編寫規(guī)范,因?yàn)檫€不清楚它應(yīng)該如何表現(xiàn)。但一般來說,編寫測(cè)試使得開發(fā)更快更穩(wěn)定。

在本教程的后面部分,你將遇到許多包含了測(cè)試的任務(wù)。所以你會(huì)看到更多的實(shí)際例子。

編寫測(cè)試需要良好的 JavaScript 知識(shí)。但我們剛剛開始學(xué)習(xí)它。因此,為了解決所有問題,到目前為止,你不需要編寫測(cè)試,但是你應(yīng)該已經(jīng)能夠閱讀測(cè)試了,即使它們比本章中的內(nèi)容稍微復(fù)雜一些。

任務(wù)


測(cè)試代碼中有什么錯(cuò)誤?

重要程度: 5

下面這個(gè) pow 的測(cè)試代碼有什么錯(cuò)誤?

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);
});

附:從語法上來說這些測(cè)試代碼是正確且通過的。


解決方案

這些測(cè)試代碼展示了開發(fā)人員在編寫測(cè)試代碼時(shí)遇到的一些疑惑。

我們這里實(shí)際上有三條測(cè)試,但是用了一個(gè)函數(shù)來放置 3 個(gè)斷言語句。

有時(shí)用這種方式編寫會(huì)更容易,但是如果發(fā)生錯(cuò)誤,那么到底什么出錯(cuò)了就很不明顯。

如果錯(cuò)誤發(fā)生在一個(gè)復(fù)雜的執(zhí)行流的中間,那么我們就必須找出那個(gè)點(diǎn)的數(shù)據(jù)。我們必須 調(diào)試測(cè)試

將測(cè)試分成多個(gè)具有明確輸入和輸出的 it 代碼塊會(huì)更好。

像是這樣:

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 代碼塊替換掉了單個(gè)的 it。現(xiàn)在,如果某個(gè)測(cè)試失敗了,我們可以清楚地看到數(shù)據(jù)是什么。

此外,我們可以通過編寫 it.only 而不是 it 來隔離單個(gè)測(cè)試,并以獨(dú)立模式運(yùn)行它:

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

  // Mocha 將只運(yùn)行這個(gè)代碼塊
  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);
  });
});


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)