Jasmine 測試反向邏輯與延遲驗證小技巧

Eric Li
hefemk
Published in
6 min readMar 29, 2024

本文記錄透過 Jasmine 撰寫單元測試時,如何進行一反向的驗證,例如「不等於」某個值、「不要執行」某個行為的測試場景,並簡單描述「延遲驗證」的使用案例。

正向驗證

一般透過 Jasmine 撰寫測試時,會見到許多正向的斷言,例如驗證一個目標值「是」什麼。

it('should return true when given an even number', () => {
expect(isEvan(2)).toBe(true); // 2 是偶數
});

反向驗證

若想要測試「不是」什麼,其實相當簡單,加上 .not 即可。

it('should return true when given an even number', () => {
expect(isEvan(1)).not.toBe(true); // 1 不是偶數
});

訂閱場景的正向驗證

但是在訂閱(Subscribe) 相關場景時,需要特別的技巧。

例如有一個通知服務 NotificationService 的實作,公開 notifications$ 予外部訂閱,當有人呼叫 notify(...) 方法則會向已訂閱者發送事件。

export class NotificationService {
private _subject = new Subject<string>();
readonly notifications$ = this._subject.asObservable();

notify(message: string): void {
this._subject.next(message);
}
}

由於 _subject 權限修飾詞是 private ,因此我們無法透過 onSpy(_subject, 'next') 介入觀察它的被呼叫過程。

這時候我們可以借助 done() 函式來驗證它確實「有」發送事件,如下:

it('should notify subscribers when an event is published', (done) => {
const service: NotificationService = getService(...);
service.notifications$.subscribe((msg: string) => {
expect(msg).toBe('hi');
done();
});
service.notify('hi');
});

訂閱場景的反向驗證

那麼如何反過來呢?

假設今天想要驗證它「不會」發送事件,透過簡單粗爆的 setTimeout() 可以達成。

it('should not notify subscribers when ...', () => {
const service: NotificationService = getService(...);
let notified = false;
service.notifications$.subscribe((msg: string) => {
notified = true;
});
service.notify('hi'); // 試著發動通知

setTimeout(
() => {
expect(notified).toBe(false); // 若仍然是 false 表示沒有發送通知
},
300 // 延遲驗證,讓訂閱有時間反應
);
});

延遲驗證

前述案例使用了 done()setTimeout() 兩種延遲驗證的手段,目的都是為了讓訂閱有時間反應,從而消減測不準的疑慮。

或許您執行上述的範例,發現不使用 setTimeout() 也可以驗證,但從我的經驗看來,仍是建議盡可能使用這些延遲技巧,以因應實務的各種變化。

例如,設想 notify(...) 的實作是非同步的,它並不是馬上會發出通知,這種情況下馬上接上 expect 來斷言是不準確的。

it('should not notify subscribers when ...', () => {
const service: NotificationService = getService(...);
let notified = false;
service.notifications$.subscribe((msg: string) => {
notified = true;
});
service.notify('hi'); // 假設 .notify 會延遲 300ms 送出通知
expect(notified).toBe(false); // 斷言過早執行,無法反應真實情況
});

若您是以 Angular 開發,在 @angular/core/testing 有提供優雅的 fakeAsync()tick() 方法,若您有 setTimeout() 敏症狀,可以參考看看。

it('should not notify subscribers when ...', fakeAsync(() => {
const service: NotificationService = getService(...);
let notified = false;
service.notifications$.subscribe((msg: string) => {
notified = true;
});
service.notify('hi'); // 假設 .notify 實作本身是非同步的,例如延遲 300ms 送出通知
tick(500); // 推進 500ms 時間
expect(notified).toBe(false);
}));

此外,還有一種利用 Jasmine 的 fail() 來主動認定的變體,一旦收到通知則視為失敗。但為了不讓測試案例沒有斷言,還是加上了一個「白目」斷言,讀者就當作是一種奇思妙想吧。

it('should not notify subscribers when ...', fakeAsync(() => {
const service: NotificationService = getService(...);
service.notifications$.subscribe((msg: string) => {
fail();
});
service.notify('hi');
tick(500);
expect(true).toBeTrue();
}));

--

--