常見問題

  1. 一般
    1. Jasmine 的下一次發行版本是什麼時候?
    2. Jasmine 的版本編號規則是什麼?
    3. 我可以在 jasmine-browser-runner 中使用外部 URL 的腳本嗎?
    4. Jasmine 可以測試 ES 模組中的程式碼嗎?
    5. 為什麼 Jasmine 允許規格中有多個預期失敗?我該如何停用它?
    6. 我該如何讓 Jasmine 讓沒有任何斷言的規格失敗?
    7. 我該如何將 Jasmine 用在我的 TypeScript 專案中?
  2. 與 Jasmine 合作的其他軟體
    1. 我可以用 Jasmine 5.x 搭配 Karma 使用嗎?
    2. 為什麼更新的 Jasmine 功能在 Karma 中無法使用?
    3. 我遇到涉及 zone.js 的問題。你們可以提供協助嗎?
    4. 我該如何將 Jasmine 比對器與 testing-library 的 waitFor 函式搭配使用?
    5. 為什麼在 webdriver.io 中 expect() 無法正常運作?
  3. 撰寫規格
    1. 我應該傳遞正規函式還是箭頭函式到 describeitbeforeEach 等?
    2. 我該如何在包含的 describebeforeEach 之前執行程式碼?Jasmine 有與 rspec 的 let 等效的函式嗎?
    3. 為什麼 Jasmine 會顯示一個沒有堆疊追蹤的例外狀況?
    4. Jasmine 是否支援參數化測試?
    5. 我該如何將更多資訊加入比對器失敗的訊息?
  4. 非同步測試
    1. 我應該使用哪一種非同步樣式,為什麼?
    2. 為什麼有些非同步規格失敗會報告為套件錯誤或其他規格的失敗?
    3. 我該如何阻止 Jasmine 並行執行我的規格?
    4. 為什麼我無法撰寫同時接受回呼並傳回承諾(或是非同步函式)的規格?我應該怎麼做?
    5. 但我真的必須測試透過不同管道發送成功和失敗訊號的程式碼。我不能(或不想)變更它。我該怎麼做?
    6. 為什麼我的非同步函式不能呼叫 `done` 多次?我應該怎麼做?
    7. 為什麼我無法傳遞非同步函式到 `describe`?我該如何從非同步載入的資料產生規格?
    8. 我該如何測試我沒有承諾或回呼的非同步行為,例如在非同步提取資料後呈現某項內容的 UI 元件?
    9. 我需要針對在測試中的程式碼結束前傳遞到非同步回呼的引數進行斷言。最好的方法是什麼?
    10. 為什麼當規格因為拒絕的承諾而失敗時,Jasmine 不總是顯示堆疊追蹤?
    11. 我收到一個未處理的承諾拒絕錯誤,但我認為這是一個誤報。
  5. 間諜
    1. 我該如何模擬 AJAX 呼叫?
    2. 為什麼我無法在某些瀏覽器中監視 localStorage 方法?我該怎麼做?
    3. 我該如何監視模組屬性?我收到類似「aProperty 不具備存取類型取得」、「未宣告為可寫或沒有設定程式」、「未宣告為可組態」的錯誤訊息。
    4. 我該如何設定間諜來傳回拒絕的承諾,而不觸發未處理的承諾拒絕錯誤?
  6. 為 Jasmine 做貢獻
    1. 我想為 Jasmine 提供協助。我該從哪裡開始?
    2. Jasmine 用什麼來測試自身?
    3. 為什麼 Jasmine 有個有趣的自編模組系統?為什麼不使用 Babel 和 Webpack?
    4. 我該如何開發依賴於某些受支援環境中遺失項目的功能?

一般

Jasmine 的下一次發行版本是什麼時候?

這取決於貢獻的速度和維護員時間的安排。

Jasmine 完全是志工的努力,這使得難以預測發行版本何時會推出,且無法承諾時間表。過去,包含新功能的發行版本通常每 1-6 個月推出一次。當發現新錯誤時,我們會盡快釋出修正套件。

回到 FAQ 索引

Jasmine 的版本編號規則是什麼?

Jasmine 盡可能徹底遵循 語意版控。這表示我們保留重大版本 (1.0、2.0 等) 來進行重大變更或其他重要作業。大部分的 Jasmine 版本最終都會成為次要版本 (2.3、2.4 等)。重大版本並不常見。

有許多人透過 jasmine 套件 (在 Node 中執行規格) 或 jasmine-browser-runner 套件 來使用 Jasmine。這些套件基於歷史原因,使用不同的版本控策略

除非在重大版本更新時,Jasmine 通常會避免停止支援瀏覽器或 Node 版本。這類例外情況包括已過生命週期的 Node 版本、我們無法在 CI 建置中做本機安裝和/或測試的瀏覽器、不再收到安全性更新的瀏覽器,以及僅在不再收到安全性更新的操作系統上執行的瀏覽器。我們會盡合理努力讓 Jasmine 繼續在那些環境中運作,但若發生中斷,則不會一定進行重大版本更新。

回到 FAQ 索引

我可以在 jasmine-browser-runner 中使用外部 URL 的腳本嗎?

您可以在 jasmine-browser.jsonjasmine-browser.js 檔案中,將指令碼網址新增至 srcFiles

  // ...
  srcFiles: [
    "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js",
    "**/*.js"
  ],
  // ...
回到 FAQ 索引

Jasmine 可以測試 ES 模組中的程式碼嗎?

可以。確切流程取決於您使用 Jasmine 的方式

回到 FAQ 索引

為什麼 Jasmine 允許規格中有多個預期失敗?我該如何停用它?

有時需要多項驗證才能確定特定結果。在這種情況下,在嘗試讓其中任何驗證通過之前,了解所有驗證皆失敗的情況會很有幫助。當某個程式碼變更會讓多項驗證通過時,這特別有用。

如果您希望各規格在遇到第一次驗證失敗時停止執行,您可以將 oneFailurePerSpec 選項設定為 true

請注意,與規格相關聯的任何 afterEach 或 afterAll 函式仍會執行。

回到 FAQ 索引

我該如何讓 Jasmine 讓沒有任何斷言的規格失敗?

預設情況下,Jasmine 不要求規格包含任何預期值。您可以透過將 failSpecWithNoExpectations 選項設定為 true 來啟用該行為

我們不建議依賴 failSpecWithNoExpectations 選項。它僅確保每個規格至少有一個預期值,而不是確保如果要驗證的行為不起作用,規格會因為正確的原因而失敗。確保規格實際上正確的唯一方法是同時嘗試這兩種形式,並在受測程式碼運作時看到它通過,在受測程式碼損毀時看到它用預定方式失敗。沒有親自動手做,極少人能持續撰寫出良好的規格,就像極少人能持續提供可運作的非測試程式碼,而不先試用一樣。

回到 FAQ 索引

我該如何將 Jasmine 用在我的 TypeScript 專案中?

有兩種常用方法可以讓 Jasmine 和 TypeScript 搭配使用。

第一種方法是使用 @babel/register,在匯入時即時將 TypeScript 檔案編譯成 JavaScript。請參閱 使用 Jasmine NPM 測試 React 應用程式,以取得範例。這種方法容易設定,並提供最快的編輯-編譯-執行-規格循環,但預設情況下不提供型別檢查。您可以透過為規格建立個別的 TypeScript 組態檔,並將 noEmit 設定成 true,以及在執行規格之前或之後在該檔案上執行 tsc,來加入型別檢查。

第二種方法是在磁碟上將 TypeScript 規格檔案編譯成 JavaScript 檔案,並組態 Jasmine 來執行編譯後的 TypeScript 檔案。這通常會提供較慢的編輯-編譯-執行-規格循環,但對於習慣已編譯語言的人來說,這是更熟悉的流程。如果您想用 TypeScript 編寫規格,並在瀏覽器中執行,這也是唯一選項。

回到 FAQ 索引

與 Jasmine 合作的其他軟體

我可以用 Jasmine 5.x 搭配 Karma 使用嗎?

有可能。karma-jasmine 5.1(撰寫本文時最新版本,且可能是最後一個版本)似乎相容於 jasmine-core 5.x。您應該可以在 package.json 中使用 NPM 覆寫,以覆寫 karma-jasmine 的依賴關係規格

{
    // ...
    "overrides": {
        "karma-jasmine": {
            "jasmine-core": "^5.0.0"
        }
    }
}
回到 FAQ 索引

為什麼更新的 Jasmine 功能在 Karma 中無法使用?

您可能會使用比您想像中舊版本的 jasmine-core。karma-jasmine 宣告相依於 jasmine-core 4.x。因此,即使您已安裝較新版本,Karma 仍然會使用 jasmine-core 4.x。您可以透過 前一個問題 中描述的方式加入 NPM 覆寫,來修復這個問題。

回到 FAQ 索引

我遇到涉及 zone.js 的問題。你們可以提供協助嗎?

請將任何與 zone.js 相關的問題報告給 Angular 專案。

Zone.js 廣泛地對 Jasmine 做 monkey patching,以它自己的實作取代許多內部關鍵元件。大部分時間它都能正常運作。但是它造成的任何問題基本上都是 zone.js 而非 Jasmine 中的錯誤。

回到 FAQ 索引

我該如何將 Jasmine 比對器與 testing-library 的 waitFor 函式搭配使用?

使用 throwUnless 而不是 expect

await waitFor(function() {
    throwUnless(myDialogElement).toHaveClass('open');
});
回到 FAQ 索引

為什麼在 webdriver.io 中 expect() 無法正常運作?

@wdio/jasmine-framework 以另一個不相容於 Jasmine 的 expect 取代了 Jasmine 的 expect。參閱 Webdriver.IO 文件 中有關其 expect API 的資訊。

除了取代 expect 之外,Webdriver.IO 也對一些 Jasmine 內部元件做了 monkey patching。只在 Webdriver.IO 存在時才會出現的錯誤應該報告給 Webdriver.IO,而不是 Jasmine。

回到 FAQ 索引

撰寫規格

我應該傳遞正規函式還是箭頭函式到 describeitbeforeEach 等?

對於 describe 來說沒差別。對於 itbeforeEachafterEach,您可能偏好使用一般函式。Jasmine 會建立一個 使用者內容,並將它作為 this 傳遞給每個 itbeforeEachafterEach 函式。這讓您可以輕鬆地在這些函式間傳遞變數,並確定它們會在每個規格後進行清理。但是這無法用於箭頭函式,因為箭頭函式中的 this 是藉由詞彙綁定的。因此如果您想使用使用者內容,您必須固守一般函式。

回到 FAQ 索引

我該如何在包含的 describebeforeEach 之前執行程式碼?Jasmine 有與 rspec 的 let 等效的函式嗎?

簡短的答案是,您不能,而且您應該重構您的測試設定,如此一來,內部的 describe 就無需還原或覆寫外部 describe 所做的設定。

當人們嘗試撰寫類似這樣的套件時,通常會遇到這個問題

// DOES NOT WORK
describe('When the user is logged in', function() {
  let user = MyFixtures.anyUser

  beforeEach(function() {
    // Do something, potentially complicated, that causes the system to run
    // with `user` logged in.
  });

  it('does some things that apply to any user', function() {
    // ...
  });

  describe('as an admin', function() {
    beforeEach(function() {
      user = MyFixtures.adminUser;
    });

    it('shows the admin controls', function() {
      // ...
    });
  });

  describe('as a non-admin', function() {
    beforeEach(function() {
      user = MyFixtures.nonAdminUser;
    });

    it('does not show the admin controls', function() {
      // ...
    });
  });
});

這是不可行的,部分原因是內部的 beforeEach 函式會在使用者已經登入後才執行。有些測試架構會提供重新排序測試設定的方式,如此一來,內部 describe 中的設定部分才能在外部 describe 的設定部分之前執行。RSpec 的 let 區塊就是一個範例。Jasmine 並未提供此功能。我們從經驗中學到,讓設定流程控制在內部和外部 describe 中來回彈動,會導致套件難以理解且難以修改。相反地,請嘗試重構設定程式碼,如此一來,每個部分都會在所有它依賴的設定之後發生。這通常表示要將外部 beforeEach 的內容取出並內嵌在內部規格或 beforeEach 中。如果這導致程式碼重複過多,則可以使用一般函式來處理,就像在非測試程式碼中所做的那樣

describe('When the user is logged in', function() {
  it('does some things that apply to any user', function() {
    logIn(MyFixtures.anyUser);
    // ...
  });

  describe('as an admin', function() {
    beforeEach(function() {
      logIn(MyFixtures.adminUser);
    });

    it('shows the admin controls', function() {
      // ...
    });
  });

  describe('as a non-admin', function() {
    beforeEach(function() {
      logIn(MyFixtures.nonAdminUser);
    });

    it('does not show the admin controls', function() {
      // ...
    });
  });

  function logIn(user) {
    // Do something, potentially complicated, that causes the system to run
    // with `user` logged in.
  }
});
回到 FAQ 索引

為什麼 Jasmine 會顯示一個沒有堆疊追蹤的例外狀況?

JavaScript 允許您拋出任何值或用任何值拒絕承諾。但是,只有 Error 物件才有堆疊追蹤。因此,如果拋出了非 Error 的值,或是有東西拒絕了一個非 Error 的承諾,則 Jasmine 無法顯示堆疊追蹤,因為沒有堆疊追蹤可以顯示。

此行為是由 JavaScript 執行時期所控制,而這不是 Jasmine 能夠變更的。

// NOT RECOMMENDED
describe('Failures that will not have stack traces', function() {
  it('throws a non-Error', function() {
    throw 'nope';
  });

  it('rejects with a non-Error', function() {
    return Promise.reject('nope');
  });
});

// RECOMMENDED
describe('Failures that will have stack traces', function() {
  it('throws an Error', function() {
    throw new Error('nope');
  });

  it('rejects with an Error', function() {
    return Promise.reject(new Error('nope'));
  });
});
回到 FAQ 索引

Jasmine 是否支援參數化測試?

不能直接這樣做。但是測試套件只是 JavaScript,所以您還是可以這樣做。

function add(a, b) {
    return a + b;
}

describe('add', function() {
    const cases = [
        {first: 3, second: 3, sum: 6},
        {first: 10, second: 4, sum: 14},
        {first: 7, second: 1, sum: 8}
    ];

    for (const {first, second, sum} of cases) {
        it(`returns ${sum} for ${first} and ${second}`, function () {
            expect(add(first, second)).toEqual(sum);
        });
    }
});
回到 FAQ 索引

我該如何將更多資訊加入比對器失敗的訊息?

當一個規格有許多類似的預期時,難以分辨哪一個失敗對應到哪一個預期

it('has multiple expectations', function() {
  expect(munge()).toEqual(1);
  expect(spindle()).toEqual(2);
  expect(frobnicate()).toEqual(3);
});
Failures:
1) has multiple expectations
  Message:
    Expected 0 to equal 1.
  Stack:
    Error: Expected 0 to equal 1.
        at <Jasmine>
        at UserContext.<anonymous> (withContextSpec.js:2:19)
        at <Jasmine>
  Message:
    Expected 0 to equal 2.
  Stack:
    Error: Expected 0 to equal 2.
        at <Jasmine>
        at UserContext.<anonymous> (withContextSpec.js:3:21)
        at <Jasmine>

有三個方法可以讓此規格的產出更清晰

以下是上述的規格,修改後使用 withContext

it('has multiple expectations with some context', function() {
  expect(munge()).withContext('munge').toEqual(1);
  expect(spindle()).withContext('spindle').toEqual(2);
  expect(frobnicate()).withContext('frobnicate').toEqual(3);
});
Failures:
1) has multiple expectations with some context
  Message:
    munge: Expected 0 to equal 1.
  Stack:
    Error: munge: Expected 0 to equal 1.
        at <Jasmine>
        at UserContext.<anonymous> (withContextSpec.js:8:40)
        at <Jasmine>
  Message:
    spindle: Expected 0 to equal 2.
  Stack:
    Error: spindle: Expected 0 to equal 2.
        at <Jasmine>
        at UserContext.<anonymous> (withContextSpec.js:9:44)
        at <Jasmine>

回到 FAQ 索引

非同步測試

我應該使用哪一種非同步樣式,為什麼?

您應該優先選擇 async/await 樣式。大多數開發者使用此種樣式編寫無錯誤規格容易得多。回傳承諾的規格較難編寫,但它們可用於較複雜的場景中。回呼樣式規格非常容易出錯,應盡可能避免使用。

回呼樣式規格有兩個主要缺點。首先是執行流程較難視覺化。這樣很容易編寫一個在 done 回呼在實際完成前呼叫的規格。第二個缺點是難以正確處理錯誤。請考慮以下規格

it('sometimes fails to finish', function(done) {
  doSomethingAsync(function(result) {
    expect(result.things.length).toEqual(2);
    done();
  });
});

如果 result.things 未定義,則存取 result.things.length 會擲回錯誤,導致無法呼叫 done。最後規格會終止,但需要經過一段時間的延遲。錯誤會被報告。但是,由於瀏覽器和節點公開有關未處理例外情況的方式,它不會包含堆疊追蹤或任何其他指出錯誤來源的資訊。

若要修正,需要將每個回呼包覆在 try-catch 中

it('finishes and reports errors reliably', function(done) {
  doSomethingAsync(function(result) {
    try {
      expect(result.things.length).toEqual(2);
    } catch (err) {
      done.fail(err);
      return;
    }

    done();
  });
});

這很費時、容易出錯,而且很可能會忘記。通常最好將回呼轉換為承諾

it('finishes and reports errors reliably', async function() {
  const result = await new Promise(function(resolve, reject) {
    // If an exception is thrown from here, it will be caught by the Promise
    // constructor and turned into a rejection, which will fail the spec.
    doSomethingAsync(resolve);
  });

  expect(result.things.length).toEqual(2);
});

在某些情況下回呼樣式規格仍然很有用。有些基於回呼的介面很難承諾化,或無法從承諾化中獲得太多好處。但大多數情況下,使用 async/await 或至少使用承諾編寫可靠的規格較容易。

回到 FAQ 索引

為什麼有些非同步規格失敗會報告為套件錯誤或其他規格的失敗?

當非同步程式碼擲回例外情況或發生未處理的承諾拒絕時,造成此情況的規格不再位於呼叫堆疊中。因此,Jasmine 無法可靠地判斷錯誤來自何處。Jasmine 最多只能將錯誤與發生時正在執行的規格或套件關聯在一起。這通常是正確的答案,因為編寫正確的規格不會在信號完成後觸發錯誤(或執行任何其他動作)。

當規格在實際完成前發出完成信號時,就會產生問題。請考慮以下兩個範例,它們都測試 doSomethingAsync 函式,此函式在完成後會呼叫回呼

// WARNING: does not work correctly
it('tries to be both sync and async', function() {
  // 1. doSomethingAsync() is called 
  doSomethingAsync(function() {
    // 3. The callback is called
    doSomethingThatMightThrow();
  });
  // 2. Spec returns, which tells Jasmine that it's done
});

// WARNING: does not work correctly
it('is async but signals completion too early', function(done) {
  // 1. doSomethingAsync() is called 
  doSomethingAsync(function() {
    // 3. The callback is called
    doSomethingThatThrows();
  });
  // 2. Spec calls done(), which tells Jasmine that it's done
  done();
});

在這兩個範例中,規格會發出完成信號,但會繼續執行,稍後會導致錯誤。錯誤發生時,Jasmine 已報告該規格已通過,並開始執行下一個規格。錯誤發生前,Jasmine 甚至可能已退出。如果發生這種情況,將不會有任何報告。

要解決此問題,請確保規格在實際完成之前不會發出完成信號。可以使用回呼來執行此操作

it('signals completion at the right time', function(done) {
  // 1. doSomethingAsync() is called 
  doSomethingAsync(function() {
    // 2. The callback is called
    doSomethingThatThrows();
    // 3. If we get this far without an error being thrown, the spec calls
    // done(), which tells Jasmine that it's done
    done();
  });
});

但是 使用 async/await 或承諾撰寫可靠的非同步規格較容易,因此我們在大部分情況下建議這樣做

it('signals completion at the right time', async function() {
  await doSomethingAsync();
  doSomethingThatThrows();
});
回到 FAQ 索引

我該如何阻止 Jasmine 並行執行我的規格?

只有在您至少使用 jasmine NPM 套件的 5.0 版,並傳遞 --parallel 命令列引數時,Jasmine 才會並行執行規格。在所有其他組態中,它一次執行一個規格(或 before/after)函式。即使並行組態也會在每個套件中依序執行規格和 before/after 函式。

然而,Jasmine 取決於那些使用者提供的函式來表示它們完成的時間。如果一個函式在實際完成之前就發出完成訊號,則下一個規格的執行將與它交錯執行。若要修正這個問題,請確保每個非同步函式僅在完全完成時才呼叫其回呼,或解析或拒絕傳回的 Promise。請參閱 非同步教學,以取得更多資訊。

回到 FAQ 索引

為什麼我無法撰寫同時接受回呼並傳回承諾(或是非同步函式)的規格?我應該怎麼做?

Jasmine 需要知道每個非同步規格何時完成,才能在適當的時間繼續執行下一個規格。如果一個規格帶有 done 回呼,其意義為「我在呼叫回呼時完成」。如果一個規格傳回一個 Promise,不論是明確傳回還是使用 async 關鍵字傳回,其意義為「我在傳回的 Promise 解決或拒絕時完成」。這兩件事不可能同時發生,而 Jasmine 也無法解決這種含糊性。後續讀者也很可能會難以理解該規格的意圖。

通常,提出這個問題的人都是面臨以下兩種情況之一。他們使用 async 只是為了能 await,並非為了向 Jasmine 發出完成訊號;或者他們試圖測試會混合多種非同步樣式的程式碼。

第一個情境:當一個規格是 async,但它只是為了能 await

// WARNING: does not work correctly
it('does something', async function(done) {
  const something = await doSomethingAsync();
  doSomethingElseAsync(something, function(result) {
    expect(result).toBe(/*...*/);
    done();
  });
});

在這種情況下,其意圖是讓規格在呼叫回呼時完成,而從規格隱含傳回的 Promise 沒有意義。最好的修正方法是變更基於回呼的函式,讓它傳回一個 Promise,然後 await 這個 Promise

it('does something', async function(/* Note: no done param */) {
  const something = await doSomethingAsync();
  const result = await new Promise(function(resolve, reject) {
    doSomethingElseAsync(something, function(r) {
      resolve(r);
    });
  });
  expect(result).toBe(/*...*/);
});

如果您想要保留回呼,則可以將 async 函式包覆在 IIFE

it('does something', function(done) {
  (async function () {
    const something = await doSomethingAsync();
    doSomethingElseAsync(something, function(result) {
      expect(result).toBe(/*...*/);
      done();
    });
  })();
});

或將 await 替換成 then

it('does something', function(done) {
  doSomethingAsync().then(function(something) {
    doSomethingElseAsync(something, function(result) {
      expect(result).toBe(170);
      done();
    });
  });
});

第二個情境:會以多種方式發出完成訊號的程式碼

// in DataLoader.js
class DataLoader {
  constructor(fetch) {
    // ...
  }

  subscribe(subscriber) {
    // ...
  }

  async load() {
    // ...
  }
}

// in DataLoaderSpec.js
// WARNING: does not work correctly
it('provides the fetched data to observers', async function(done) {
  const fetch = function() {
    return Promise.resolve(/*...*/);
  };
  const subscriber = function(result) {
    expect(result).toEqual(/*...*/);
    done();
  };
  const subject = new DataLoader(fetch);

  subject.subscribe(subscriber);
  await subject.load(/*...*/);
});

就像第一個情境一樣,這個規格的問題在於它會以兩種不同的方式發出完成訊號:透過解決(解決或拒絕)隱含傳回的 Promise,以及呼叫 done 回呼。這反映了 DataLoader 類別中潛在的設計問題。通常,人們會寫出這樣的規格,因為無法依賴受測程式碼以一致的方式發出完成訊號。呼叫訂閱者的順序和所解決的傳回 Promise 可能無法預測。更糟糕的是,DataLoader 可能只使用傳回的 Promise 發出失敗訊號,而讓它在成功狀態下仍保持待處理。對於有這樣問題的程式碼,很難寫出可信賴的規格。

修正方法是變更受測程式碼,讓它一直以一致的方式發出完成訊號。在這個案例中,其意義是確保 DataLoader 的最後一個動作,在成功和失敗狀態下都是解決或拒絕傳回的 Promise。然後就可以像這樣可靠地對它進行測試

it('provides the fetched data to observers', async function(/* Note: no done param */) {
  const fetch = function() {
    return Promise.resolve(/*...*/);
  };
  const subscriber = jasmine.createSpy('subscriber');
  const subject = new DataLoader(fetch);

  subject.subscribe(subscriber);
  // Await the returned promise. This will fail the spec if the promise
  // is rejected or isn't resolved before the spec timeout.
  await subject.load(/*...*/);
  // The subscriber should have been called by now. If not,
  // that's a bug in DataLoader, and we want the following to fail.
  expect(subscriber).toHaveBeenCalledWith(/*...*/);
});

另請參閱 如何在受測程式碼完成之前對傳遞給非同步回呼引數進行驗證

回到 FAQ 索引

但我真的必須測試透過不同管道發送成功和失敗訊號的程式碼。我不能(或不想)變更它。我該怎麼做?

您可以將兩邊都轉換成 Promise,如果它們不是 Promise。然後使用 Promise.race 來等待率先解決或拒絕的 Promise

// in DataLoader.js
class DataLoader {
  constructor(fetch) {
    // ...
  }

  subscribe(subscriber) {
    // ...
  }

  onError(errorSubscriber) {
    // ...
  }

  load() {
    // ...
  }
}

// in DataLoaderSpec.js
it('provides the fetched data to observers', async function() {
  const fetch = function() {
    return Promise.resolve(/*...*/);
  };
  let resolveSubscriberPromise, rejectErrorPromise;
  const subscriberPromise = new Promise(function(resolve) {
    resolveSubscriberPromise = resolve;
  });
  const errorPromise = new Promise(function(resolve, reject) {
    rejectErrorPromise = reject;
  });
  const subject = new DataLoader(fetch);

  subject.subscribe(resolveSubscriberPromise);
  subject.onError(rejectErrorPromise);
  const result = await Promise.race([subscriberPromise, errorPromise]);

  expect(result).toEqual(/*...*/);
});

請注意,這假設受測程式碼會發出成功或失敗訊號,但絕不會同時發出這兩個訊號。通常,無法為在失敗時可能發出成功和失敗訊號的非同步程式碼寫出可信賴的規格。

回到 FAQ 索引

為什麼我的非同步函式不能呼叫 `done` 多次?我應該怎麼做?

在 Jasmine 2.x 和 3.x 中,一個 以非同步回呼為基礎的非同步函數 可以多次呼叫其 已完成 回呼,只有第一次呼叫會執行某些動作。這種機制是為了防止 已完成 被呼叫超過一次時,Jasmine 破壞其內部狀態。

我們從那以後學到,非同步函數只有在實際完成時才會發出完成訊號是很重要的。當一個規格在告訴 Jasmine 它已經完成後仍在繼續執行時,它會與其他規格的執行交錯。這可能會造成諸如間歇性測試失敗、未報告失敗,或 在錯誤的規格上報告失敗 等問題。多年來,這些問題一直是造成使用者困惑和錯誤回報的常見原因。Jasmine 4 嘗試透過在一個非同步函數呼叫 已完成 超過一次時回報錯誤訊息,來使其更容易診斷這些問題。

如果您有一個呼叫 已完成 多次的規格,最好的作法是重新改寫它,使其僅呼叫 已完成 一次。有關規格多次發出完成訊號的常見情況和建議的修正方式,請參閱 此相關的常見問題解答

如果您真的無法消除多餘的已完成呼叫,您可以透過將 已完成 封裝在一個忽略所有呼叫除了第一次呼叫的函數中來實作 Jasmine 2-3 的行為,如下所示。但請注意,執行這種作業的規格仍然有錯誤,而且很可能造成上述問題。

function allowUnsafeMultipleDone(fn) {
  return function(done) {
    let doneCalled = false;
    fn(function(err) {
      if (!doneCalled) {
        done(err);
        doneCalled = true;
      }
    });
  }
}

it('calls done twice', allowUnsafeMultipleDone(function(done) {
  setTimeout(done);
  setTimeout(function() {
    // This code may interleave with subsequent specs or even run after Jasmine
    // has finished executing.
    done();
  }, 50);
}));
回到 FAQ 索引

為什麼我無法傳遞非同步函式到 `describe`?我該如何從非同步載入的資料產生規格?

同步函數無法呼叫非同步函數,而 描述 必須是同步的,因為它用於同步的背景中,例如透過腳本標籤載入的腳本。讓它變成非同步的會導致所有使用 Jasmine 的現有程式碼中斷,並使得 Jasmine 在最受歡迎的環境中無法使用。

但是,如果您使用 ES 模組,您可以在呼叫頂層 描述 之前非同步地取得資料。方法如下

// WARNING: does not work
describe('Something', async function() {
   const scenarios = await fetchSceanrios();
   
   for (const scenario of scenarios) {
       it(scenario.name, function() {
           // ...
       });
   }
});

執行這個

const scenarios = await fetchSceanrios();

describe('Something', function() {
   for (const scenario of scenarios) {
       it(scenario.name, function() {
           // ...
       });
   }
});

要使用頂層 等待,您的規格檔案必須是 ES 模組。如果您在瀏覽器中執行規格,您需要使用 jasmine-browser-runner 2.0.0 或更新版本,並將 "enableTopLevelAwait": true 新增到組態檔中。

回到 FAQ 索引

我該如何測試我沒有承諾或回呼的非同步行為,例如在非同步提取資料後呈現某項內容的 UI 元件?

有兩種基本方法可以解決這個問題。第一個作法是讓非同步行為立即完成(或盡可能接近立即完成),然後在規格中 等待。以下是使用 enzymejasmine-enzyme 函式庫來測試 React 組件的此方法範例

describe('When data is fetched', () => {
  it('renders the data list with the result', async () => {
    const payload = [/*...*/];
    const apiClient = {
      getData: () => Promise.resolve(payload);
    };

    // Render the component under test
    const subject = mount(<DataLoader apiClient={apiClient} />);
    
    // Wait until after anything that's already queued
    await Promise.resolve();
    subject.update();

    const dataList = subject.find(DataList);
    expect(dataList).toExist();
    expect(dataList).toHaveProp('data', payload);
  });
});

請注意,規格等待的 Promise 與傳遞給受測程式碼的 Promise 無關。人們通常在兩個地方使用相同的 Promise,但只要傳遞給受測程式碼的 Promise 已解析,這並不重要。重要的是規格中的 等待 呼叫發生於受測程式碼中的呼叫之後。

當事情出錯時,此方法簡單,有效率,並且能快速失敗。但當受測程式碼會執行一個以上的 await.then() 時,難以正確安排時程。受測程式碼中非同步操作的變更很容易會中斷規格,需要新增其他 await

另一種方法是在需求行為發生之前執行輪詢

describe('When data is fetched', () => {
  it('renders the data list with the result', async () => {
    const payload = [/*...*/];
    const apiClient = {
      getData: () => Promise.resolve(payload);
    };

    // Render the component under test
    const subject = mount(<DataLoader apiClient={apiClient} />);

    // Wait until the DataList is rendered
    const dataList = await new Promise(resolve => {
      function poll() {
        subject.update();
        const target = subject.find(DataList);

        if (target.exists()) {
          resolve(target);
        } else {
          setTimeout(poll, 50);
        }
      }
      poll();
    });
    
    expect(dataList).toHaveProp('data', payload);
  });
});

這一開始會稍微複雜一點,並且效率會稍微低一點。假設預期的組件未立即呈示,這還會超時(預設 5 秒),而不是立即失敗。但在變更方面,它的復原力較強。假設有更多 await.then() 呼叫新增至受測程式碼,它仍然會通過。

在撰寫第二種形式的規格時,你可能會發現 DOM Testing LibraryReact Testing Library 很實用。這兩個函式庫內的 findBy*findAllBy* 查詢已實作上述輪詢行為。

回到 FAQ 索引

我需要針對在測試中的程式碼結束前傳遞到非同步回呼的引數進行斷言。最好的方法是什麼?

考慮一下會擷取資料、呼叫任何已註冊的回呼函式、執行一些清除動作,然後最後會解析回傳的承諾的 DataFetcher 類別。驗證傳給回呼函式的引數的最佳方法是在回呼函式中儲存引數,然後在發出完成訊號前,斷言它們具有正確的值

it("calls the onData callback with the expected args", async function() {
  const subject = new DataFetcher();
  let receivedData;
  subject.onData(function(data) {
    receivedData = data;
  });

  await subject.fetch();

  expect(receivedData).toEqual(expectedData);
});

你也可以透過使用間諜程式來取得更好的錯誤訊息

it("calls the onData callback with the expected args", async function() {
  const subject = new DataFetcher();
  const callback = jasmine.createSpy('onData callback');
  subject.onData(callback);

  await subject.fetch();

  expect(callback).toHaveBeenCalledWith(expectedData);
});

人們很常會撰寫類似這樣的東西

// WARNING: Does not work
it("calls the onData callback with the expected args", async function() {
  const subject = new DataFetcher();
  subject.onData(function(data) {
    expect(data).toEqual(expectedData);
  });

  await subject.fetch();
});

假設 onData 回呼函式從未被呼叫,這段程式碼會不正確地通過,因為預期從未執行。以下為另一種常見但錯誤的方法

// WARNING: Does not work
it("calls the onData callback with the expected args", function(done) {
  const subject = new DataFetcher();
  subject.onData(function(data) {
    expect(data).toEqual(expectedData);
    done();
  });

  subject.fetch();
});

在該版本中,規格在受測程式碼實際執行完畢前發出完成訊號。這可能會導致規格的執行與其他規格發生競合,這會導致 錯誤轉送及其他問題

回到 FAQ 索引

為什麼當規格因為拒絕的承諾而失敗時,Jasmine 不總是顯示堆疊追蹤?

這與 為什麼 Jasmine 會顯示沒有堆疊追蹤的例外狀況? 類似。假設承諾已拒絕,並以 Error 物件作為原因,例如 Promise.reject(new Error("out of cheese")),則 Jasmine 會顯示與錯誤相關聯的堆疊追蹤。假設承諾已拒絕,但沒有原因或非 Error 原因,則 Jasmine 沒有堆疊追蹤可顯示。

回到 FAQ 索引

我收到一個未處理的承諾拒絕錯誤,但我認為這是一個誤報。

了解 JavaScript 執行時間會決定哪些承諾拒絕被視為未處理,而不是 Jasmine 一事非常重要。Jasmine 只會回應 JavaScript 執行時間發出的未處理拒絕事件。

如果你允許控制權傳回 JavaScript 執行時間,而未先附加拒絕處理常式,通常只建立拒絕的承諾就足以觸發未處理的承諾拒絕事件。即便你尚未執行任何承諾相關事項,這都是成立的。Jasmine 會將未處理的拒絕轉換成失敗,原因是它們幾乎總是表示有問題出乎意料地發生,而且由於無法區分「真正的」未處理拒絕與最終會在未來處理的拒絕。

考慮此規格

it('causes an unhandled rejection', async function() {
  const rejected = Promise.reject(new Error('nope'));
  await somethingAsync();
  try {
    await rejected;
  } catch (e) {
    // Do something with the error
  }
});

拒絕最終將透過 try/catch 來處理。但在規格執行該部分之前,JS 執行環境會偵測到未處理的拒絕。原因是 await somethingAsync() 呼叫將控制權傳回給 JS 執行環境。不同的 JS 執行環境會以不同的方式偵測到未處理的拒絕,但常見的行為是在將控制權傳回給執行環境之前,如果已附加捕捉處理常式給拒絕,則不會認為拒絕是未處理的。在大部分情況下,可以透過重新安排程式碼順序來達成。

it('causes an unhandled rejection', async function() {
  const rejected = Promise.reject(new Error('nope'));
  let rejection;
  try {
    await rejected;
  } catch (e) {
    rejection = e;
  }
  await somethingAsync();
  // Do something with `rejection`
});

作為最後的手段,您可以透過附加無動作捕捉處理常式來抑制未處理的拒絕。

it('causes an unhandled rejection', async function() {
  const rejected = Promise.reject(new Error('nope'));
  rejected.catch(function() { /* do nothing */ });
  await somethingAsync();
  let rejection;
  try {
    await rejected;
  } catch (e) {
   rejection = e;
  }
  // Do something with `rejection`
});

另請參閱 如何設定 spy 以傳回遭拒絕的承諾而不會觸發未處理承諾拒絕錯誤?了解在設定 spy 時如何避免未處理的拒絕。

如上所示,Jasmine 並未判斷哪些拒絕屬於未處理的。請不要開啟問題要求我們更改。

回到 FAQ 索引

間諜

我該如何模擬 AJAX 呼叫?

如果您使用的是 XMLHttpRequest 或任何在幕後使用它的函式庫,jasmine-ajax 是不錯的選擇。它能處理模擬 XMLHttpRequest 這個有時複雜的細節,並提供一個用於驗證請求和存根回應的良好 API。

XMLHttpRequest 不同,較新的 HTTP 用戶端 API,例如 axiosfetch,很容易使用 Jasmine spy 手動模擬。只需將 HTTP 用戶端注入到受測程式碼中

async function loadThing(thingId, thingStore, fetch) {
  const url = `http://example.com/api/things/{id}`;
  const response = await fetch(url);
  thingStore[thingId] = response.json();
}

// somewhere else
await loadThing(thingId, thingStore, fetch);

然後在規格中,注入一個 spy

describe('loadThing', function() {
  it('fetches the correct URL', function() {
    const fetch = jasmine.createSpy('fetch')
      .and.returnValue(new Promise(function() {}));

    loadThing(17, {}, fetch);

    expect(fetch).toHaveBeenCalledWith('http://example.com/api/things/17');
  });

  it('stores the thing', function() {
    const payload = return {
      id: 17,
      name: 'the thing you requested'
    };
    const response = {
      json: function() {
        return payload;
      }
    };
    const thingStore = {};
    const fetch = jasmine.createSpy('fetch')
      .and.returnValue(Promise.resolve(response));

    loadThing(17, thingStore, fetch);

    expect(thingStore[17]).toEqual(payload);
  });
});
回到 FAQ 索引

為什麼我無法在某些瀏覽器中監視 localStorage 方法?我該怎麼做?

這會在部分瀏覽器中通過,但在 Firefox 和 Safari 17 中會失敗。

it('sets foo to bar on localStorage', function() {
    spyOn(localStorage, 'setItem');
    localStorage.setItem('foo', 'bar');
    expect(localStorage.setItem).toHaveBeenCalledWith('foo', 'bar');
});

作為安全性措施,Firefox 和 Safari 17 不允許覆寫 localStorage 的屬性。將它們指定給 spyOn 在幕後執行的操作,是一個無動作。這是瀏覽器實施的限制,Jasmine 無法解決。

一個替代方案是檢查 localStorage 的狀態,而不是驗證已對它進行哪些呼叫

it('sets foo to bar on localStorage', function() {
   localStorage.setItem('foo', 'bar');
   expect(localStorage.getItem('foo')).toEqual('bar');
});

另一個選項是建立一個包裝器將其包圍住 localStorage 及模擬該包裝器。

回到 FAQ 索引

我該如何監視模組屬性?我收到類似「aProperty 不具備存取類型取得」、「未宣告為可寫或沒有設定程式」、「未宣告為可組態」的錯誤訊息。

註:這些常見問答題涉及到一個快速變化的領域,並且可能會過時。它最後在 2023 年 9 月更新。

此錯誤表示某個項目 (可能是轉譯器,但可能是 JavaScript 執行環境) 已經將模組的匯出屬性標記為唯讀。ES 模組規格要求匯出的模組屬性為唯讀,有些轉譯器會遵循該需求,即使在發出 CommonJS 模組時也是如此。如果屬性標記為唯讀,Jasmine 無法用 spy 取代它。

不論您身處什麼樣的環境,您都可以針對您想模擬的事物使用依賴注入,並從規格中注入 spy 或模擬物件,以避免這個問題。這個方法通常會改善規格和受測程式碼的可維護性。需要模擬模組通常表示程式碼連結過緊,最好修正其連結,而不是透過測試工具來解決問題。

根據您身處的環境,您可能會啟用模組模擬。請參閱 模組模擬指南 以了解更多資訊。

回到 FAQ 索引

我該如何設定間諜來傳回拒絕的承諾,而不觸發未處理的承諾拒絕錯誤?

了解 JavaScript 執行時間會決定哪些承諾拒絕被視為未處理,而不是 Jasmine 一事非常重要。Jasmine 只會回應 JavaScript 執行時間發出的未處理拒絕事件。

如果允許控制權回到 JavaScript 執行環境,卻沒有附加拒絕處理常式,只需建立一個遭到拒絕的承諾就足以觸發節點和大多數瀏覽器中的未處理拒絕事件。即使您對這個承諾不執行任何動作也是如此。Jasmine 會將未處理的拒絕轉換為失敗,因為它們幾乎總是表示某個項目意外地出錯。(另請參閱:我收到未處理的承諾拒絕錯誤,但我認為這是誤判。)

考慮此規格

it('might cause an unhandled promise rejection', async function() {
  const foo = jasmine.createSpy('foo')
    .and.returnValue(Promise.reject(new Error('nope')));
  await expectAsync(doSomething(foo)).toBeRejected();
});

規格會建立一個 rejected promise。如果一切運作正常,它會被處理,最終會由非同步比對器處理。但如果 class="language-plaintext highlighter-rouge">doSomething 無法呼叫 class="language-plaintext highlighter-rouge">foo 或無法傳遞 rejection,瀏覽器或 Node 會觸發未處理的 Promise rejection 事件。Jasmine 會將這視為在事件發生時執行的組或規格的失敗。

一種解決方法是僅在實際呼叫 spy 時建立 rejected promise

it('does not cause an unhandled promise rejection', async function() {
  const foo = jasmine.createSpy('foo')
    .and.callFake(() => Promise.reject(new Error('nope')));
  await expectAsync(doSomething(foo)).toBeRejected();
});

透過使用 rejectWith spy strategy 可以讓這一點更明確

it('does not cause an unhandled promise rejection', async function() {
  const foo = jasmine.createSpy('foo')
    .and.rejectWith(new Error('nope'));
  await expectAsync(doSomething(foo)).toBeRejected();
});

如上所示,Jasmine 並未判斷哪些拒絕屬於未處理的。請不要開啟問題要求我們更改。

回到 FAQ 索引

參與貢獻

我想為 Jasmine 提供協助。我該從哪裡開始?

感謝您的協助!Jasmine 團隊只有有限的時間開發 Jasmine,因此我們非常感謝來自社群的所有協助。

Github 問題回報

當回報的 GitHub 問題看起來像是 Jasmine 能支援的內容時,我們會標記「need help」標籤給該問題。此標籤表示我們相信對話中包含足夠的資訊,讓某人能自行實作。(我們並不總是正確的。如果您有進一步的問題,請提出詢問。)

新點子

您是否有 GitHub 問題回報中尚未涵蓋到的點子?歡迎提出建議。我們建議(但不要求)您在提交 pull request 前先開啟一個問題來討論您的點子。我們並非對每個建議都說「是」,因此在付出大量工作前先詢問會比較好。

回到 FAQ 索引

Jasmine 用什麼來測試自身?

Jasmine 使用 Jasmine 來測試 Jasmine。

Jasmine 的測試組會載入 Jasmine 的兩個副本。第一個會從 class="language-plaintext highlighter-rouge">lib/ 中的建置檔案載入。第二個稱為 class="language-plaintext highlighter-rouge">jasmineUnderTest,會直接從 class="language-plaintext highlighter-rouge">src/ 中的原始檔載入。第一個 Jasmine 用於執行規格,而規格會在 class="language-plaintext highlighter-rouge">jasmineUnderTest 中呼叫函式。

這有幾個好處

如果您想知道這是如何設定的,請參閱 requireCore.jsdefineJasmineUnderTest.js

回到 FAQ 索引

為什麼 Jasmine 有個有趣的自編模組系統?為什麼不使用 Babel 和 Webpack?

簡而言之,Jasmine 早於 Babel 和 Webpack,而且轉換成這些工具需要投入大量工作,卻只有較少的回報,這基本上在 Jasmine 不再支援非 ES2017 環境(例如 Internet Explorer)時就不再需要了。儘管 Jasmine 的大部分內容仍以 ES5 編寫,但現在可以使用更新的語言功能。

在 Jasmines 的生命週期的大部分時間中,它需要在不支援較新的 JavaScript 功能的瀏覽器上執行。這表示編譯的程式碼無法使用較新的語法和函式庫功能,例如箭頭函式,async/awaitPromiseSymbolMapSet。因此,它是使用 ES5 語法撰寫,不使用任何非可移植的函式庫功能,但某些特定範圍的情況下除外,例如非同步比對器。

那麼,為什麼不採用 Babel 及 Webpack?部分原因是 Jasmine 所處在一個奇怪的空間,打破了這些工具的一些假設:它既是應用程式也是函式庫,即使它作為應用程式執行時,也無法安全地修改 JavaScript 執行環境。如果 Jasmine 為遺失的函式庫功能加入多重填充,則可能導致依賴這些功能的程式碼在其沒有這些功能的瀏覽器上不正確地通過規格。我們尚未找出如何配置 Babel 及 Webpack(或任何其他綑綁器),以保證不會引入任何多重填充。即使我們做到了,回報可能也相對較小。撰寫 ES5 語法,而非 ES6 語法,是支援廣泛瀏覽器最簡單的部分。最困難的部分在於處理遺失的函式庫功能及其他不相容問題,仍必須手動解決。

Jasmine 現有的建置工具擁有簡單、快速的優點,並且需要極低的維護成本。如果變更帶來重大改善,我們並不反對改用較新的工具。但到目前為止,在這個領域保持保守讓我們得以略過相當多的前端建置工具變動,並利用時間處理對使用者有益的事情。

回到 FAQ 索引

我該如何開發依賴於某些受支援環境中遺失項目的功能?

我們嘗試讓所有支援的瀏覽器和 Node 版本都可以使用 Jasmine 的所有功能,但有時這沒有道理。例如,即使 Jasmine 在 4.0.0 之前持續執行於沒有 Promise 的環境中,但 2.7.0 中還是加入了對傳回 Promise 規格的支援。要撰寫不適用於所有環境的事物的規格,請檢查是否具有必要的語言/執行時間功能,若否,請將規格標記為待處理。請參閱 spec/helpers/checkForUrl.js,以及它定義的 requireUrls 函式用法,以了解如何執行此操作。

請參閱 src/core/base.js 中的 is* 方法,針對以下範例說明如何安全地檢查物件是否為可能不存在類型的執行個體。

回到 FAQ 索引

是否有我們錯過的疑問?

請開啟 問題提交請求