上一篇說明了如何撰寫在資料繫結情境的單元測試,這一篇我們把要設定給元件屬性的資料,改成透過 Angular 服務跟後端程式取得,來了解如何撰寫這種情境的測試程式。
這一篇會使用 ProductPageComponent
頁面元件,這個元件會把 ProductService
服務所取得產品資料,以產品卡片 (ProductCardComponent
) 的方式顯示在頁面上。
這支元件相依了 ProductService
服務與 ProductCardComponent
元件。前者利用 HttpClient 跟後端服務取得資料;後者則使用了 Angular Material 與 ShoppingCartService
。因此,如前一篇所述,需要將這些相依對象加入測試模組的設定內。
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
HttpClientModule,
MatButtonModule,
MatIconModule,
MatCardModule,
MatSnackBarModule,
],
declarations: [ProductPageComponent, ProductCardComponent],
providers: [ShoppingCartService, ProductService],
}).compileComponents();
fixture = TestBed.createComponent(ProductPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
ProductPageComponent
這個頁面元件,在一開始會利用 ProductService
服務來透過 HttpClient 取得後端服務傳回資料。因為在單元測試中並不會相依任何外部資源,所以在測試上我們必須變更此服務的作業,使得在執行測試時不會使用實際的後端服務。
為此,我們可以自訂一個 ProductSpyService
服務,並在服務內實作 getProducts()
方法,讓此方法直接傳回 3 筆產品資料。
const products = [
new Product({ id: 1, name: '產品 A', price: 999 }),
new Product({ id: 2, name: '產品 B', price: 200 }),
new Product({ id: 3, name: '產品 C', price: 10 }),
];
@Injectable({
providedIn: 'root',
})
export class ProductSpyService {
getProducts(): Observable<Product[]> {
return of(products);
}
}
接下來,利用 providers
陣列來變更測試模組內的服務。
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ ... ],
declarations: [ ... ],
providers: [
ShoppingCartService,
{ provide: ProductService, useClass: ProductSpyService },
],
}).compileComponents();
...
});
最後,就可以依 ProductSpyService
的 getProducts()
方法所傳回的筆數,來驗證頁面元件所顯示的結果。
it('當後端服務回傳 3 筆產品資料, 頁面應顯示 3 個產品卡片', () => {
// Arrange
var cards = fixture.debugElement.queryAll(
By.directive(ProductCardComponent)
);
// Act
// Assert
expect(cards.length).toBe(3);
});
spyOn
設定特定服務方法的回傳值如果元件所相依的服務有多個方法,或是依不同的測試情境需要傳回不同值的時候,利用上述方法會需要建立不少的 Spy 服務,而 Jasmine 提供了兩個方式讓我們可以很方便的去建立 Spy 服務。
第一種方法,在 providers
屬性的設定上會使用實際的 ProductService
服務。
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ ... ],
declarations: [ ... ],
providers: [
ShoppingCartService,
ProductService,
],
}).compileComponents();
...
});
在測試程式中,如下面程式,首先利用 TestBed.inject
方法來取得注入的 ProductService
服務實體,並且利用 spyOn
方法設定該服務的 getProducts
方法的回傳值。
it('當後端服務回傳 3 筆產品資料, 頁面應顯示 3 個產品卡片', () => {
// Arrange
const productService = TestBed.inject(ProductService);
spyOn(productService, 'getProducts').and.returnValue(of(products));
// Act
component.ngOnInit();
fixture.detectChanges();
// Assert
var cards = fixture.debugElement.queryAll(
By.directive(ProductCardComponent)
);
expect(cards.length).toBe(3);
});
最後,就可以觸發元件的 ngOnInit()
生命週期事件與變更檢測,進而去驗證頁面上的 ProductCardComponent
元件的個數是否為 3 。
jasmine.createSpyObj
方法建立假服務第二個方式可以利用 Jasmine 提供的 createSpyObj
方法來建立假服務,此方法會回傳一 jsamine.SpyObj<>
型別物件,因此一開始會宣告 productService
變數。
let productService: jasmine.SpyObj<ProductService>;
其次,會利用 createSpyObj<ProductService>
來建立一 Spy 服務,此方法可以傳入 Spy 服務需要的方法名稱。接下來,就會去設定 getProducts
方法預計的回傳資料,以及使用 useValue
方式來替代 ProductService
服務。
beforeEach(async () => {
productService = jasmine.createSpyObj<ProductService>(['getProducts']);
productService.getProducts.and.returnValue(of(products));
await TestBed.configureTestingModule({
imports: [ ... ],
declarations: [ ... ],
providers: [
ShoppingCartService,
{ provide: ProductService, useValue: productService },
],
}).compileComponents();
...
});
最後,就可以撰寫與自訂假服務一樣的測試案例。
it('當後端服務回傳 3 筆產品資料, 頁面應顯示 3 個產品卡片', () => {
// Arrange
var cards = fixture.debugElement.queryAll(
By.directive(ProductCardComponent)
);
// Act
// Assert
expect(cards.length).toBe(3);
});
文章的最後就來執行 ng test
。
這一篇介紹了三種建立假服務物件的方法,讓我們可以在單元測試不與 HttpClient 相依,完整的測試程式可以參考 GitHub 中。接下來就更進一步的說明在這種假服務物件的情境下,有什麼其他測試的方法可以使用。