非同步工作

非同步碼在現代 JavaScript 應用程式中很常見。其測試方式大多與測試同步碼相同,只有一個主要區別:Jasmine 需要知道非同步工作何時完成。

Jasmine 支援使用三種方式管理非同步工作:async/await、承諾和回呼。如果 Jasmine 沒有偵測到其中一種方式,它會假設這項工作是同步的,並開始處理佇列中的下一項,只要該功能能返回即可。這些機制都適用於 beforeEachafterEachbeforeAllafterAllit

async/await

通常,撰寫非同步測試最簡便的方式是使用 async/awaitasync 功能會隱式傳回一個承諾。Jasmine 會等到傳回的承諾已解決或拒絕,才會繼續處理佇列中的下一項。被拒絕的承諾會導致規格失敗,或針對 beforeAllafterAll 會導致套件層級失敗。

beforeEach(async function() {
  await someLongSetupFunction();
});

it('does a thing', async function() {
  const result = await someAsyncFunction();
  expect(result).toEqual(someExpectedValue);
});

承諾

如果需要更嚴密的控制,您可以顯式傳回承諾。Jasmine 會視任何含有 then 方法的物件為承諾,因此您可以使用 JavaScript 執行時間內建的 Promise 類型,或使用函式庫。

beforeEach(function() {
  return new Promise(function(resolve, reject) {
    // do something asynchronous
    resolve();
  });
});

it('does a thing', function() {
  return someAsyncFunction().then(function (result) {
    expect(result).toEqual(someExpectedValue);
  });
});

回呼

使用回呼撰寫非同步測試也是可行的。這是一個較低層級的機制,而且容易發生錯誤,但它可用於測試以回呼為基礎的碼,或測試那些無法用承諾表達的碼。如果傳送至 Jasmine 的功能需要參數(傳統上稱為 done),則 Jasmine 會傳送一個非同步工作完成後要啟用的功能。

至為重要的是,done 回呼必須只呼叫一次,而且呼叫 done 必須是非同步功能,或任何它所呼叫的功能所執行的最後一項工作。在撰寫回呼式非同步測試時,一個常見的錯誤是在測試中的碼仍執行時呼叫 done。在這種情況下,done 呼叫後產生的錯誤,可能會與造成錯誤的規格無關,甚至根本不會報告。

beforeEach(function(done) {
  setTimeout(function() {
    // do some stuff
    done();
  }, 100);
});


it('does a thing', function(done) {
  someAsyncFunction(function(result) {
    expect(result).toEqual(someExpectedValue);
    done();
  });
});

處理失敗

有時候,非同步碼無法正常運作,而您會希望規格正確失敗。所有未處理的錯誤會被 Jasmine 擷取,並傳送到目前執行的規格。有時您需要明確地使您的規格失敗。

使用承諾失敗

被拒絕的 Promise 會導致規格失敗,就像執行錯誤後發生的情況一樣。

beforeEach(function() {
  return somePromiseReturningFunction();
});

it('does a thing', function() {
  // Since `.then` propagates rejections, this test will fail if
  // the promise returned by asyncFunctionThatMightFail is rejected.
  return asyncFunctionThatMightFail().then(function(value) {
    // ...
  });
});

function somePromiseReturningFunction() {
  return new Promise(function(resolve, reject) {
    if (everythingIsOk()) {
      resolve();
    } else {
      reject();
    }
  });
}

使用 async/await 失敗

async/await 功能可以透過傳回被拒絕的承諾,或執行錯誤來指出失敗。

beforeEach(async function() {
  // Will fail if the promise returned by
  // someAsyncFunction is rejected.
  await someAsyncFunction();
});

it('does a thing', async function() {
  // Will fail if doSomethingThatMightThrow throws.
  doSomethingThatMightThrow();

  // Will fail if the promise returned by
  // asyncFunctionThatMightFail is rejected.
  const value = await asyncFunctionThatMightFail();
  // ...
});

使用回呼失敗

當作回呼傳遞的 done 函式也可以使用 done.fail(),來讓規範失敗,可選擇傳遞訊息或 Error 物件。

beforeEach(function(done) {
  setTimeout(function() {
    try {
      riskyThing();
      done();
    } catch (e) {
      done.fail(e);
    }
  });
});

done 函式也會偵測直接傳遞給它的 Error,來讓規範失敗。

beforeEach(function(done) {
  setTimeout(function() {
    let err = null;

    try {
      riskyThing();
    } catch (e) {
      err = e;
    }

    done(err);
  });
});

回報器

回報器事件處理器也可以使用任何一種方法來變成非同步的。請注意,所有回報器事件都已接收資料,因此如果您使用回呼方法,done 回呼應該是最後一個參數。

使用模擬時鐘來避免撰寫非同步測試

如果某個操作變成非同步只因為它仰賴 setTimeout 或其他基於時間的行為,要測試它的好方法就是使用 Jasmine 的 模擬時鐘,讓它同步執行。這種測試的撰寫方式比較簡單,而且執行速度會比實際等待時間到期的非同步測試還快。

function doSomethingLater(callback) {
  setTimeout(function() {
    callback(12345);
  }, 10000);
}

describe('doSomethingLater', function() {
  beforeEach(function() {
    jasmine.clock().install();
  });

  afterEach(function() {
    jasmine.clock().uninstall();
  });

  it('does something after 10 seconds', function() {
    const callback = jasmine.createSpy('callback');
    doSomethingLater(callback);
    jasmine.clock().tick(10000);
    expect(callback).toHaveBeenCalledWith(12345);
  });
});