Angular - Viết unit test với Mock và Spy
Bài đăng này đã không được cập nhật trong 3 năm
Trước khi đào sâu vào chủ đề của bài viết, tôi sẽ giới thiệu qua về việc viết unit test trong Angular.
Angular sử dụng Jasmine
và Karma
để viết và chạy test.
Jasmine
là một javascript testing framework hỗ trợ BDD (Behavior Driven Development), nó cố gắng mô tả các tests trong một định dạng chúng ta có thể dễ dàng đọc hiểu, ngay cả đối với những người không am hiểu kĩ thuật cũng có thể hiểu những gì đang test ở đây.Karma
là một test runner, nó sinh ra browser và chạy các jasmine tests bên trong nó từ command line. Chúng ta có thể theo dõi kết quả ở browser được sinh ra hoặc ở command line.Karma
có cơ chế theo dõi sự thay đổi của codes để tự động chạy lại test.
Mục tiêu của bài viết
- Biết cách Mock với một fake class
- Biết cách Mock bằng cách extend class và override hàm
- Biết cách Mock sử dụng
Spy
Sample Code
Ví dụ tôi có một LoginComponent
, inject vào đó là AuthenticationService
. Để đơn giản hóa việc test, ở đây tôi chỉ sử dụng AuthenticationService
để kiểm tra người dùng đã được xác thực hay chưa, nếu chưa, LoginComponent
sẽ hiển thị một nút Login
để bắt người dùng phải đăng nhập trước khi muốn sử dụng trang web của tôi.
LoginComponent:
# login.component.ts
import { Component } from '@angular/core';
import { AuthenticationService } from './authentication.service';
@Component({
selector: 'app-login',
template: `<a [hidden]="isLoggedIn()">Login</a>`
})
export class LoginComponent {
constructor(private authService: AuthenticationService) {
}
isLoggedIn() {
return this.authService.isAuthenticated();
}
}
AuthenticationService:
# authentication.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class AuthUserService {
isAuthenticated(): boolean {
return !!localStorage.getItem('token');
}
}
Trên thực tế, sẽ không có một trang web nào chỉ kiểm tra sự xuất hiện của key token
trong localStorage
để kết luận được rằng người dùng này đã được xác thực hay chưa, nhưng vì mục đích đơn giản nên là chúng ta sẽ kiểm tra như vậy đi ^^.
Ở những phần tiếp theo, tôi sẽ đi vào giới thiệu một số cách để có thể test được LoginComponent
kia nha.
Viết test với một AuthenticationService
thực
Chúng ta có thể test LoginComponent
bằng cách sử dụng một instance thực của AuthenticationService
. Với cách này, chúng ta sẽ cần phải cài đặt một số dữ liệu lưu trong localStorage
.
# login.component.spec.ts
import { LoginComponent } from './login.component';
import { AuthenticationService } from "./authentication.service";
describe('LoginComponent', () => {
let component: LoginComponent;
let service: AuthenticationService;
beforeEach(() => {
service = new AuthenticationService();
component = new LoginComponent(service);
});
afterEach(() => {
localStorage.removeItem('token');
});
it('isLoggedIn returns false when the user is not authenticated', () => {
expect(component.isLoggedIn()).toBeFalsy();
});
it('isLoggedIn returns true when the user is authenticated', () => {
localStorage.setItem('token', 'dG9rZW4=');
expect(component.isLoggedIn()).toBeTruthy();
});
});
Cách này có vẻ không được ổn cho lắm, khi mà chúng ta cần phải biết rõ bên trong AuthenticationService
hoạt động như thế nào. Thử tưởng tượng, nếu như LoginComponent
inject thêm một vài service khác nữa, chúng ta cũng sẽ cần phải biết rõ cơ chế hoạt động của từng service đó.
Ngoài ra, ví dụ như AuthenticationService
thay đổi cơ chế lưu trữ token, chẳng hạn lưu trữ trong sessionStorage
hay cookies
thay vì localStorage
, khi đó test cho LoginComponent
dĩ nhiên sẽ bị fail ở case isLoggedIn returns true when the user is authenticated
bởi vì token đang được lưu ở trong localStorage
mà.
Đây là lý do tại sao chúng ta nên viết test cho các class một cách cô lập, chúng ta chỉ cần quan tâm đến LoginComponent
, không cần phải bận tâm đến những class phụ thuộc của nó khi viết test.
Chúng ta sẽ làm được điều đó bằng cách Mock
các class phụ thuộc của LoginComponent
. Mock
là một hành động tạo ra một cái gì đó trông có vẻ giống với những class phụ thuộc, nhưng chúng ta có thể điều khiển kết quả của chúng trong test một cách dễ dàng. Sau đây là một số cách để tạo ra các mock
Mock với fake class
Trong cách này, chúng ta tạo một fake class của AuthenticationService
có tên là MockAuthService
. Trong MockAuthService
chúng ta cũng tạo ra hàm isAuthenticated()
giống như trong AuthenticationService
, nhưng sẽ có thể điều khiển kết quả trả về của hàm đó dễ dàng hơn, mà không cần biết rõ xử lý thực sự bên trong của nó ra sao. Hãy xem đoạn codes ở dưới để rõ hơn nhé
# login.component.spec.ts
import { LoginComponent } from './login.component';
class MockAuthService {
authenticated = false;
isAuthenticated() {
return this.authenticated;
}
}
describe('Component: Login', () => {
let component: LoginComponent;
let service: MockAuthService;
beforeEach(() => {
service = new MockAuthService();
component = new LoginComponent(service);
});
it('isLoggedIn returns false when the user is not authenticated', () => {
service.authenticated = false;
expect(component.isLoggedIn()).toBeFalsy();
});
it('isLoggedIn returns true when the user is authenticated', () => {
service.authenticated = true;
expect(component.isLoggedIn()).toBeTruthy();
});
});
Ta có thể thấy, kết quả trả về của hàm isAuthenticated()
có thể dễ dàng được điều khiển bằng cách thay đổi giá trị của thuộc tính authenticated
bên trong MockAuthService
. Đồng thời, nếu như có những sự thay đổi ở bên trong hàm isAuthenticated
của class AuthenticationService
, các test của chúng ta vẫn sẽ luôn đúng ^^.
Mock bằng cách extend class và override hàm
Thỉnh thoảng, việc tạo ra một fake class hoàn chỉnh có thể sẽ trở nên phức tạp, mất thời gian khi class thực chứa nhiều hàm khác cũng cần phải có để có thể hoạt động được.
Để khắc phục nhược điểm trên, chúng ta có thể tạo một class extend từ AuthenticationService
và override lại hàm nào mà chúng ta cần sử dụng cho việc test trên:
class MockAuthService extends AuthenticationService {
authenticated = false;
isAuthenticated() {
return this.authenticated;
}
}
Class MockAuthService
ở trên extend từ AuthenticationService
. Nó có thể truy cập được vào tất cả các hàm và thuộc tính trong AuthenticationService
, nhưng chỉ override lại hàm isAuthenticated
thôi, vì vậy chúng ta có thể dễ dàng điều khiển được hành vi của nó và viết test một cách cô lập.
Mock sử dụng Spy
Spy
là một tính năng của Jasmine
framework, cho phép chúng ta mock giá trị trả về của các hàm trong một instance của class.
Để mock giá trị trả về của isAuthenticated
trong service
nhận giá trị true
, chúng ta sử dụng Spy
như sau:
spyOn(service, 'isAuthenticated').and.returnValue(true);
Hãy viết lại đoạn test trên sử dụng Spy
trên một instance thực của AuthenticationService
xem thế nào nha:
# login.component.spec.ts
import { LoginComponent } from './login.component';
import { AuthenticationService } from "./authentication.service";
describe('LoginComponent', () => {
let component: LoginComponent;
let service: AuthenticationService;
beforeEach(() => {
service = new AuthenticationService();
component = new LoginComponent(service);
});
it('isLoggedIn returns false when the user is not authenticated', () => {
spyOn(service, 'isAuthenticated').and.returnValue(false);
expect(component.isLoggedIn()).toBeFalsy();
expect(service.isAuthenticated).toHaveBeenCalled();
});
it('isLoggedIn returns true when the user is authenticated', () => {
spyOn(service, 'isAuthenticated').and.returnValue(true);
expect(component.isLoggedIn()).toBeTruthy();
expect(service.isAuthenticated).toHaveBeenCalled();
});
});
Kết luận
Trên đây tôi đã giới thiệu cho các bạn một số cách test trong Angular bằng cách tạo các Mock
sử dụng fake class, extend một class hoặc sử dụng một instance thực của class với Spy
. Việc tạo các Mock
giúp chúng ta viết test cho các class một cách cô lập, dễ dàng hơn, không cần phải bận tâm đến cơ chế làm việc bên trong của các class phụ thuộc. Hi vọng sẽ giúp ích cho bạn đọc trong việc viết unit test trong các dự án Angular nhé ^^.
Tài liệu tham khảo
All rights reserved