上一篇說明了如何撰寫 Angular 服務的測試程式。在實務上,我們常會在 Angular 服務中,利用 HttpClient 來請求後端服務,來取得與處理 Angular 應用程式的資料,這一篇就來看看這種存取後端服務的 Angular 服務要如何撰寫單元測試。
這一篇會透過 ProductService
服務 (檔名為 services/product.service.ts
) 來說明單元程式的撰寫。
@Injectable({
providedIn: 'root',
})
export class ProductService {
private _url = 'http://localhost:3000/products';
constructor(private httpClient: HttpClient) {}
getProducts(): Observable<Product[]> {
return this.httpClient.get<Product[]>(this._url).pipe(
mergeMap((products) => products),
map((product) => new Product(product)),
toArray()
);
}
}
這支程式會透過 json-server 的 API 來模擬請求後端服務,因此若希望執行此範例程式,需要在 Terminal 執行 npm install -g json-server
命令來安裝 json-server。接著,一樣在 Terminal 執行 json-server src/assets/db.json
命令,就可以透過 GET、POST、PUT 與 DELETE 等 HTTP 方法來存取 db.json
內的資料內容。
如同範例程式,在正式的產品程式碼中,我們會在模組中去引用 HttpClientModule
模組,並在Angular 服務的建構式去注入 HttpClient
,以便可以使用 HttpClient
的 get
等方法來呼叫後端服務。不過在單元測試並不會與外部資源有任何的依賴性,因此我們必須在單元測試中利用假的 HttpClient
物件來模擬與驗證程式執行的結果。
為此 Angular 提供了 HttpClientTestingModules
來讓我們可以使用模擬的 httpClient
物件,所以我們可以在 TestBed 的設定中去引用此模組,並取得型別為 HttpClientController
的 HttpClient
模擬物件,此兩者皆需從 @angular/common/http/testing
匯入。
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
describe('ProductService', () => {
let service: ProductService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
service = TestBed.inject(ProductService);
httpMock = TestBed.inject(HttpTestingController);
});
});
接下來,因為 getProducts
方法是回傳 Observable<Product>
物件,所以在呼叫此方法時需要被訂閱,否則不會真的執行此方法。而在訂閱方法內,則會去驗證所取得到的結果是否與預期相同,因此測試程式會寫成:
it('應該可以取得產品資料', () => {
// Arrange
// Act
service.getProducts().subscribe((actual) => {
expect(actual).toEqual([
new Product({ id: 1, name: '產品 A', price: 999 }),
new Product({ id: 2, name: '產品 B', price: 200 }),
new Product({ id: 3, name: '產品 C', price: 10 }),
new Product({ id: 4, name: '產品 D', price: 150 }),
]);
});
// Assert
...
});
然後,在單元測試中的執行並不會實際去後端取得資料,主要使用 HttpTestingController
物件來模擬 httpClient
物件的行為。因此在驗證上會利用 expectOne
方法,來檢查所請求的 URL 是否被請求過一次,進而驗證所發送的請求是否使用預期的方法。最後利用 flush
方法來決定這個請求要傳回什麼樣的結果,這個結果就會是訂閱方法所得到的結果。
it('應該可以取得產品資料', () => {
// Arrange
// Act
service.getProducts().subscribe((actual) => {
expect(actual).toEqual([
new Product({ id: 1, name: '產品 A', price: 999 }),
new Product({ id: 2, name: '產品 B', price: 200 }),
new Product({ id: 3, name: '產品 C', price: 10 }),
new Product({ id: 4, name: '產品 D', price: 150 }),
]);
});
// Assert
const testRequest = httpMock.expectOne('http://localhost:3000/products');
expect(testRequest.request.method).toBe('GET');
testRequest.flush([
{ id: 1, name: '產品 A', price: 999 },
{ id: 2, name: '產品 B', price: 200 },
{ id: 3, name: '產品 C', price: 10 },
{ id: 4, name: '產品 D', price: 150 },
]]);
});
除了驗證請求的 HTTP 方法外,也可以驗證請求的 body,來確認產品程式在對後端服務發送請求時,所傳送的資訊參數是否正確。
expect(testRequest.request.body).toEqual(...);
測試 HTTP 方法之後,還會透過 verify()
方法檢查是否有預期外的請求;因為這個動作會是每個測試 HTTP 的案例完成後執行,所以會在 afterEach
中呼叫這個方法。
afterEach(() => {
httpMock.verify();
});
最後在 Terminal 執行 ng test
命令來確認測試程式的驗證結果。
這一篇說明了如何測試使用了 HttpClient
的 Angular 服務,完整的測試程式放在 GitHub 中。接下來,就針對在 Angular 應用程式中最常使用的元件 (Component),來看看如何撰寫元件的單元程式。