本文記錄透過 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();
}));