在單元測試真實世界代碼時,許多情況使測試難以編寫。如何檢查是否調用了函數?如何測試 Ajax 調用?或者使用 setTimeout
的代碼?這時,您需要使用 測試替身 —— 替換代碼,使難以測試的內容易於測試。
多年來,Sinon.js 一直是 JavaScript 測試中創建測試替身的實際標準。對於任何編寫測試的 JavaScript 開發人員來說,它都是必不可少的工具,因為沒有它,為真實應用程序編寫測試幾乎是不可能的。
最近,一個名為 testdouble.js 的新庫正在興起。它擁有與 Sinon.js 類似的功能集,只是這里和那裡有一些不同。
在本文中,我們將探討 Sinon.js 和 testdouble.js 提供的內容,並比較它們各自的優缺點。 Sinon.js 是否仍然是更好的選擇,或者挑戰者能否勝出?
注意:如果您不熟悉測試替身,建議您先閱讀我的 Sinon.js 教程。這將幫助您更好地理解我們將在此處討論的概念。
為了確保易於理解正在討論的內容,以下是所用術語的快速概述。這些是 Sinon.js 的定義,在其他地方可能略有不同。
需要注意的是,testdouble.js 的目標之一是減少這種術語之間的混淆。
讓我們首先看看 Sinon.js 和 testdouble.js 在基本用法上的比較。
Sinon 有三種不同的測試替身概念:間諜、存根和模擬。其思想是,每種都代表不同的使用場景。這使得該庫對於來自其他語言或閱讀過使用相同術語的書籍(例如 xUnit 測試模式)的人來說更加熟悉。但另一方面,這三種概念也可能使 Sinon 在首次使用時 更難 理解。
這是一個 Sinon 用法的基本示例:
<code class="language-javascript">// 以下是查看函数调用的参数的方法: var spy = sinon.spy(Math, 'abs'); Math.abs(-10); console.log(spy.firstCall.args); // 输出:[ -10 ] spy.restore(); // 以下是控制函数执行方式的方法: var stub = sinon.stub(document, 'createElement'); stub.returns('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' stub.restore();</code>
相反,testdouble.js 選擇了一個更簡單的 API。它不使用間諜或存根之類的概念,而是使用 JavaScript 開發人員更熟悉的語言,例如 td.function
、td.object
和 td.replace
。這使得 testdouble 潛在地更容易上手,並且更適合某些任務。但另一方面,某些更高級的用途可能根本不可能實現(這有時是故意的)。
以下是 testdouble.js 的使用方法:
<code class="language-javascript">// 以下是查看函数调用的参数的方法: var abs = td.replace(Math, 'abs'); Math.abs(-10); var explanation = td.explain(abs); console.log(explanation.calls[0].args); // 输出:[ -10 ] // 以下是控制函数执行方式的方法: var createElement = td.replace(document, 'createElement'); td.when(createElement(td.matchers.anything())).thenReturn('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' // testdouble 使用一次调用重置所有测试替身,无需单独清理 td.reset();</code>
testdouble 使用的語言更簡單。我們“替換”函數而不是“存根”它。我們要求 testdouble “解釋”函數以從中獲取信息。除此之外,到目前為止,它與 Sinon 相當相似。
這也擴展到創建“匿名”測試替身:
<code class="language-javascript">var x = sinon.stub();</code>
與
<code class="language-javascript">var x = td.function();</code>
Sinon 的間諜和存根具有提供有關它們的更多信息的屬性。例如,Sinon 提供了 stub.callCount
和 stub.args
等屬性。在 testdouble 的情況下,我們從 td.explain
獲取此信息:
<code class="language-javascript">// 我们也可以为测试替身命名 var x = td.function('hello'); x('foo', 'bar'); td.explain(x); console.log(x); /* 输出: { name: 'hello', callCount: 1, calls: [ { args: ['foo', 'bar'], context: undefined } ], description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n - called with `("foo", "bar")`.', isTestDouble: true } */</code>
較大的區別之一與設置存根和驗證的方式有關。使用 Sinon,您可以在存根之後鏈接命令,並使用斷言來驗證結果。 testdouble.js 只需向您展示您希望如何調用函數——或者如何“排練”函數調用。
<code class="language-javascript">var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');</code>
與
<code class="language-javascript">var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));</code>
這使得 testdouble 的 API 更易於理解,因為您不需要知道可以在何時鏈接哪些操作。
在高層次上,這兩個庫都相當相似。但是您可能需要在實際項目中執行的常見測試任務呢?讓我們看看一些差異開始顯現的情況。
首先要注意的是,testdouble.js 沒有“間諜”的概念。雖然 Sinon.js 允許我們替換函數調用以便從中獲取信息,同時保留函數的默認行為,但這在 testdouble.js 中根本不可能。當您使用 testdouble 替換函數時,它總是會丟失其默認行為。
但這不一定是問題。間諜最常見的用法是使用它們來驗證是否調用了回調,這很容易使用 td.function
來完成:
<code class="language-javascript">var spy = sinon.spy(); myAsyncFunction(spy); sinon.assert.calledOnce(spy);</code>
與
<code class="language-javascript">var spy = td.function(); myAsyncFunction(spy); td.verify(spy());</code>
雖然不是什麼大問題,但仍然需要注意這兩個庫之間的這種差異,否則如果您期望能夠以某種更具體的方式使用 testdouble.js 中的間諜,您可能會感到驚訝。
您會遇到的第二個區別是 testdouble 對輸入更嚴格。
Sinon 的存根和斷言都允許您對提供的參數不精確。這最容易通過示例來說明:
<code class="language-javascript">var stub = sinon.stub(); stub.withArgs('hello').returns('foo'); console.log(stub('hello', 'world')); // 输出:'foo' sinon.assert.calledWith(stub, 'hello'); // 没有错误</code>
與
<code class="language-javascript">// 以下是查看函数调用的参数的方法: var spy = sinon.spy(Math, 'abs'); Math.abs(-10); console.log(spy.firstCall.args); // 输出:[ -10 ] spy.restore(); // 以下是控制函数执行方式的方法: var stub = sinon.stub(document, 'createElement'); stub.returns('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' stub.restore();</code>
默認情況下,Sinon 不關心向函數提供了多少額外參數。雖然它提供了諸如 sinon.assert.calledWithExactly
之類的函數,但在文檔中並不建議將其作為默認值。像 stub.withArgs
這樣的函數也沒有“exactly”變體。
另一方面,testdouble.js 默認情況下要求指定精確的參數。這是設計使然。其思想是,如果向函數提供了一些在測試中未指定的其他參數,則這可能是一個錯誤,並且應該使測試失敗。
可以在 testdouble.js 中允許指定任意參數,但這並不是默認值:
<code class="language-javascript">// 以下是查看函数调用的参数的方法: var abs = td.replace(Math, 'abs'); Math.abs(-10); var explanation = td.explain(abs); console.log(explanation.calls[0].args); // 输出:[ -10 ] // 以下是控制函数执行方式的方法: var createElement = td.replace(document, 'createElement'); td.when(createElement(td.matchers.anything())).thenReturn('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' // testdouble 使用一次调用重置所有测试替身,无需单独清理 td.reset();</code>
使用 ignoreExtraArgs: true
,行為類似於 Sinon.js。
雖然使用 Sinon.js 的 Promise 並不復雜,但 testdouble.js 具有返回和拒絕 Promise 的內置方法。
<code class="language-javascript">var x = sinon.stub();</code>
與
<code class="language-javascript">var x = td.function();</code>
注意:可以使用 sinon-as-promised 在 Sinon 1.x 中包含類似的便捷函數。 Sinon 2.0 和更新版本以 stub.resolves
和 stub.rejects
的形式包含 Promise 支持。
Sinon 和 testdouble 都提供了一種簡單的方法來讓存根函數調用回調。但是,它們在工作方式上有一些差異。
Sinon 使用 stub.yields
來讓存根調用它作為參數接收的 第一個函數。
<code class="language-javascript">// 我们也可以为测试替身命名 var x = td.function('hello'); x('foo', 'bar'); td.explain(x); console.log(x); /* 输出: { name: 'hello', callCount: 1, calls: [ { args: ['foo', 'bar'], context: undefined } ], description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n - called with `("foo", "bar")`.', isTestDouble: true } */</code>
testdouble.js 默認使用 Node 樣式模式,其中回調被假定為 最後一個 參數。在排練調用時,您也不必指定它:
<code class="language-javascript">var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');</code>
使 testdouble 的回調支持更強大的原因是您可以輕鬆定義具有多個回調或回調順序不同的場景的行為。
假設我們想調用 callback1
……
<code class="language-javascript">var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));</code>
請注意,我們將 td.callback
作為參數傳遞給 td.when
中的函數。這告訴 testdouble 我們希望使用哪個參數作為回調。
使用 Sinon,也可以更改行為:
<code class="language-javascript">var spy = sinon.spy(); myAsyncFunction(spy); sinon.assert.calledOnce(spy);</code>
在這種情況下,我們使用 callsArgWith
而不是 yields
。我們必須提供調用的特定索引才能使其工作,這可能有點麻煩,尤其是在具有許多參數的函數上。
如果我們想使用某些值調用 兩個 回調呢?
<code class="language-javascript">var spy = td.function(); myAsyncFunction(spy); td.verify(spy());</code>
使用 Sinon,這根本不可能。您可以鏈接對 callsArgWith
的多次調用,但它只會調用其中一個。
除了能夠使用 td.replace
替換函數之外,testdouble 還允許您替換整個模塊。
這主要在您有一個直接導出需要替換的函數的模塊的情況下有用:
<code class="language-javascript">var stub = sinon.stub(); stub.withArgs('hello').returns('foo'); console.log(stub('hello', 'world')); // 输出:'foo' sinon.assert.calledWith(stub, 'hello'); // 没有错误</code>
如果我們想用 testdouble 替換它,我們可以使用 td.replace('path/to/file')
,例如……
<code class="language-javascript">// 以下是查看函数调用的参数的方法: var spy = sinon.spy(Math, 'abs'); Math.abs(-10); console.log(spy.firstCall.args); // 输出:[ -10 ] spy.restore(); // 以下是控制函数执行方式的方法: var stub = sinon.stub(document, 'createElement'); stub.returns('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' stub.restore();</code>
雖然 Sinon.js 可以替換某個對象的成員函數,但它不能像這樣替換模塊。在使用 Sinon 時要執行此操作,您需要使用另一個模塊,例如 proxyquire 或 rewire。
<code class="language-javascript">// 以下是查看函数调用的参数的方法: var abs = td.replace(Math, 'abs'); Math.abs(-10); var explanation = td.explain(abs); console.log(explanation.calls[0].args); // 输出:[ -10 ] // 以下是控制函数执行方式的方法: var createElement = td.replace(document, 'createElement'); td.when(createElement(td.matchers.anything())).thenReturn('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' // testdouble 使用一次调用重置所有测试替身,无需单独清理 td.reset();</code>
關於模塊替換需要注意的另一件事是 testdouble.js 會自動替換整個模塊。如果它像這裡的示例一樣是函數導出,它會替換該函數。如果它是一個包含多個函數的對象,它會替換所有這些函數。構造函數和 ES6 類也受支持。 proxyquire 和 rewire 都要求您單獨指定要替換的內容和方式。
如果您使用 Sinon 的模擬計時器、模擬 XMLHttpRequest 或模擬服務器,您會注意到它們在 testdouble 中不存在。
模擬計時器可以作為插件使用,但 XMLHttpRequests 和 Ajax 功能需要以不同的方式處理。
一個簡單的解決方案是替換您正在使用的 Ajax 函數,例如 $.post
:
<code class="language-javascript">var x = sinon.stub();</code>
對於 Sinon.js 的初學者來說,一個常見的絆腳石往往是清理間諜和存根。 Sinon 提供了 三種 不同的方法來做到這一點,這並沒有什麼幫助。
<code class="language-javascript">var x = td.function();</code>
或:
<code class="language-javascript">// 我们也可以为测试替身命名 var x = td.function('hello'); x('foo', 'bar'); td.explain(x); console.log(x); /* 输出: { name: 'hello', callCount: 1, calls: [ { args: ['foo', 'bar'], context: undefined } ], description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n - called with `("foo", "bar")`.', isTestDouble: true } */</code>
或:
<code class="language-javascript">var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');</code>
通常情況下,建議使用 sandbox 和 sinon.test
方法,否則很容易意外地留下存根或間諜,這可能會導致其他測試出現問題。這可能會導致難以追踪的級聯故障。
testdouble.js 只提供了一種清理測試替身的方法:td.reset()
。推薦的方法是在 afterEach
鉤子中調用它:
<code class="language-javascript">var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));</code>
這極大地簡化了測試替身的設置和測試後的清理,降低了難以追踪的錯誤的可能性。
我們現在已經了解了這兩個庫的功能。它們都提供了一套相當類似的功能集,但它們彼此的設計理念有所不同。我們能否將其分解為優缺點?
讓我們首先談談 Sinon.js。它提供了一些比 testdouble.js 更多的附加功能,並且它的一些方面更易於配置。這為它在更特殊的測試場景中提供了一些更高的靈活性。 Sinon.js 還使用更熟悉其他語言的人的語言——間諜、存根和模擬等概念存在於不同的庫中,並且也在測試相關的書籍中討論過。
缺點是增加了複雜性。雖然它的靈活性允許專家做更多的事情,但這意味著某些任務比在 testdouble.js 中更複雜。對於那些剛接觸測試替身概念的人來說,它也可能具有更陡峭的學習曲線。事實上,即使像我這樣非常熟悉它的人也可能難以詳細說明 sinon.stub
和 sinon.mock
之間的某些區別!
testdouble.js 選擇了一個更簡單的接口。它的大部分內容都相當簡單易用,並且感覺更適合 JavaScript,而 Sinon.js 有時感覺像是為其他語言設計的。由於這一點及其一些設計原則,它對於初學者來說更容易上手,即使是經驗豐富的測試人員也會發現許多任務更容易完成。例如,testdouble 使用相同的 API 來設置測試替身和驗證結果。由於其更簡單的清理機制,它也可能更不容易出錯。
testdouble 最大的問題是由它的一些設計原則造成的。例如,完全缺乏間諜可能會使某些更喜歡使用間諜而不是存根的人無法使用它。這在很大程度上是一個意見問題,您可能根本不會發現問題。除此之外,儘管 testdouble.js 是一個更新的庫,但它正在為 Sinon.js 提供一些嚴重的競爭。
以下是按功能的比較:
功能 | Sinon.js | testdouble.js |
---|---|---|
间谍 | 是 | 否 |
存根 | 是 | 是 |
延迟存根结果 | 否 | 是 |
模拟 | 是 | 是1 |
Promise 支持 | 是(在 2.0 中) | 是 |
时间辅助函数 | 是 | 是(通过插件) |
Ajax 辅助函数 | 是 | 否(改为替换函数) |
模块替换 | 否 | 是 |
内置断言 | 是 | 是 |
匹配器 | 是 | 是 |
自定义匹配器 | 是 | 是 |
参数捕获器 | 否2 | 是 |
代理测试替身 | 否 | 是 |
td.replace(someObject)
來實現類似的效果。 stub.yield
(不要與 stub.yields
混淆)來實現與參數捕獲器類似的效果。 Sinon.js 和 testdouble.js 都提供了一套相當類似的功能。在這方面,兩者都不明顯優越。
兩者之間最大的區別在於它們的 API。 Sinon.js 可能稍微冗長一些,同時提供了許多關於如何做事情的選項。這可能是它的優點和缺點。 testdouble.js 具有更精簡的 API,這使其更容易學習和使用,但由於其更武斷的設計,有些人可能會發現它有問題。
您是否同意 testdouble 的設計原則?如果是,那麼沒有理由不使用它。我在許多項目中都使用過 Sinon.js,我可以肯定地說 testdouble.js 至少完成了我在 Sinon.js 中完成的 95% 的工作,其餘 5% 可能可以通過一些簡單的解決方法來完成。
如果您發現 Sinon.js 難以使用,或者正在尋找更“JavaScript 式”的測試替身方法,那麼 testdouble.js 也可能適合您。即使像我這樣花了很多時間學習使用 Sinon 的人,我也傾向於建議嘗試 testdouble.js 並看看您是否喜歡它。
但是,testdouble.js 的某些方面可能會讓那些了解 Sinon.js 或其他經驗豐富的測試人員頭疼。例如,完全缺乏間諜可能是決定性因素。對於專家和那些想要最大靈活性的用戶來說,Sinon.js 仍然是一個不錯的選擇。
如果您想了解更多關於如何在實踐中使用測試替身的信息,請查看我的免費 Sinon.js 實戰指南。雖然它使用 Sinon.js,但您也可以將相同的技術和最佳實踐應用於 testdouble.js。
有問題?評論?您是否已經在使用 testdouble.js 了?閱讀本文後,您是否會考慮嘗試一下?請在下面的評論中告訴我。
本文由 James Wright、Joan Yin、Christian Johansen 和 Justin Searls 共同評審。感謝所有 SitePoint 的同行評審者,使 SitePoint 內容達到最佳狀態!
Sinon.js 和 Testdouble.js 都是流行的 JavaScript 測試庫,但它們有一些關鍵區別。 Sinon.js 以其豐富的功能集而聞名,包括間諜、存根和模擬,以及用於模擬計時器和 XHR 的實用程序。它是一個多功能工具,可以與任何測試框架結合使用。另一方面,Testdouble.js 是一個極簡的庫,專注於為測試替身提供簡單直觀的 API,測試替身是待測系統部分的替代品。它不包括用於模擬計時器或 XHR 的實用程序,但它設計易於使用和理解,因此對於那些更喜歡更精簡的測試方法的人來說是一個不錯的選擇。
Sinon.js 和 Testdouble.js 都可以通過 npm(Node.js 包管理器)安裝。對於 Sinon.js,您可以使用命令 npm install sinon
。對於 Testdouble.js,命令是 npm install testdouble
。安裝後,您可以分別使用 const sinon = require('sinon')
和 const td = require('testdouble')
在您的測試文件中引入它們。
是的,可以在同一個項目中同時使用 Sinon.js 和 Testdouble.js。它們都設計得非常簡潔,並且可以很好地與其他庫一起工作。但是,請記住它們具有重疊的功能,因此同時使用它們可能會導致混淆。通常建議根據您的具體需求和偏好選擇其中一個。
在 Sinon.js 中,您可以使用 sinon.spy()
創建間諜。此函數返回一個間諜對象,該對象記錄對其進行的所有調用,包括參數、返回值和異常。在 Testdouble.js 中,您可以使用 td.function()
創建間諜。此函數返回一個記錄所有調用的測試替身函數,包括參數。
在 Sinon.js 中,您可以使用 sinon.stub()
創建存根。此函數返回一個存根對象,其行為類似於間諜,但它還允許您定義其行為,例如指定返回值或拋出異常。在 Testdouble.js 中,您可以使用 td.when()
創建存根。此函數允許您在使用特定參數調用測試替身時定義其行為。
在 Sinon.js 中,您可以使用 spy.called
、spy.calledWith()
和 spy.returned()
等方法來驗證間諜或存根。在 Testdouble.js 中,您可以使用 td.verify()
來斷言測試替身是否以某種方式被調用。
與 Testdouble.js 相比,Sinon.js 具有更全面的功能集。它包括用於模擬計時器和 XHR 的實用程序,這對於測試某些類型的代碼非常有用。它也更廣泛地使用,並且擁有更大的社區,這意味著可以獲得更多資源和支持。
與 Sinon.js 相比,Testdouble.js 具有更簡單直觀的 API。它設計易於使用和理解,因此對於那些更喜歡更精簡的測試方法的人來說是一個不錯的選擇。它還通過使濫用測試替身變得困難來鼓勵良好的測試實踐。
是的,Sinon.js 和 Testdouble.js 都設計得非常簡潔,並且可以很好地與其他測試框架一起工作。它們可以與任何支持 JavaScript 的測試框架一起使用。
是的,Sinon.js 和 Testdouble.js 在其官方網站上都有大量的文檔。還有許多教程、博客文章和在線課程涵蓋了這些庫的深入內容。
以上是JavaScript測試工具攤牌:sinon.js vs testdouble.js的詳細內容。更多資訊請關注PHP中文網其他相關文章!