Anglular - Mocking Child Components

Trong quán trình phát triển phần mềm, song song với phần code các chức năng thì để đem bảo hững đoạn mã code đó hoạt động đúng yêu cầu và giảm thiểu phát sinh lỗi, chúng ta thường viết các unit test song song với quá trình code. Việc này với nhiều dev đánh giá là còn mất thời gian nhiều bằng cả quá trình code chức năng, đây cũng là nỗi ác mộng của nhưng newbie. Trong Angular 2+ việc viết test cũng như vậy, nó có nhiều thứ để test như để đảm bảo các thành phần trang, logic và sự tương tác giữa các thành phần. Trong Angular chúng ta đã quen với khái niệm component, khi thực thi code các component có thể không ngang hàng có thể là component này lại nằm trong compoent kia, gì vậy việc viết test cho những trường hợp này là không tránh khỏi, bài viết này tôi sẽ hướng dẫn một số phương pháp để viết test cho trường hợp nêu trên. Tôi sẽ xem xét 4 cách tiếp cận khác nhau để testing components với child components. Đây là, theo thứ tự ít được khuyến nghị nhất:

  1. Thêm child component vào declarations array
  2. Sử dụng NO_ERRORS_SCHEMA để bỏ qua child component
  3. Mock/stub thủ công child component
  4. Sử dụng ngMocks để tự động mock child component

Tuy nhiên chúng sẽ phải đảm bảo những chuẩn mực nhất định nào đó, bài viết này tôi viết unit test dựa trên những tiêu chí sau:

  1. More isolated testing: Tôi muốn kiểm tra các service và component được đề cập, không phụ thuộc vào chúng.
  2. Strongly typed template checking: Nếu một child component thay đổi, chúng tôi muốn đảm bảo rằng các unit test phụ thuộc bị phá vỡ.
  3. Less coupling between components and services: Dependency Injection giúp dễ dàng inject services, nhưng nếu các service đó đang được sử dụng trên toàn bộ, việc tái cấu trúc trở nên khó khăn.
  4. Prefer dumb components over smart components: Dumb components(nghĩa là các thành phần phụ thuộc vào đầu vào và đầu ra) dễ test hơn nhiều so với các thành phần phụ thuộc vào nhiều service.

Để viest test trường hợp này tôi sẽ đưa ra một ví dụ đơn giản như sau. Có một ParentComponent và nó includes một component khác là ChildComponent

Tất cả ParentComponent sẽ rendering ChildComponent, từ đó hiển thị danh sách trẻ em được hard-coded. Hãy để viết test để kiểm tra thành phần này.

Đây là đoạn code tôi định sẽ dùng để test.

Khi chạy đoạn code trên nó sẽ sai với lỗi như sau:

Failed: Template parse errors: 
'child' is not a known element:
1. If 'child' is an Angular component, then verify that it is part of this module. 
2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. (" <div>I am a parent. These are my children:</div>

Vậy cách giải quyết ở đây là gì? Tôi đưa ra 4 sự lựa chọn, các bạn có thể sử dụng nó từ thời điểm và tùy vào mục đích của mình.

Cách 1: Thêm ChildComponent vào declarations array

Ok, nếu thêm như ChildComponent như trên thì nó thành công. Tuy nhiên bằng cách này, tôi đã phá vỡ quy tắc số 1: Tests shalt be isolated. Bằng cách khai báo ChildComponent, test của tôi hiện phụ thuộc vào nó để thành công. Điều gì xảy ra nếu chúng ta thêm một dependen vào ChildComponent?

Thêm Router như là một dependency vào ChildComponent.

Tôi có thể import RouterTestingModule vào unit test củatôi,nhưng tôi không thể giải quyết Router và unit test thất bại lần nữa:

Error: StaticInjectorError(DynamicTestModule)[ChildComponent -> Router]:    
StaticInjectorError(Platform: core)[ChildComponent -> Router]:
NullInjectorError: No provider for Router!

Mặc dù chúng tôi luôn có thể import RouterTestingModule, đây rõ ràng là một giải pháp không tốt. Các thay đổi đối với logic bên trong của ChildComponent nên không yêu cầu tôi sửa đổi các unit test ở ParentComponent. Với 2 thành phần thì không sao, tuy nhiên nếu là 20 hoặc 200 thì sẽ rất khó xử lý.

Cách 2: NO_ERRORS_SCHEMA

Nếu bạn đã thực hiện tìm kiếm Google cho lỗi trên, có lẽ bạn sẽ gặp khá nhiều bài viết về Stack Overflow khuyên bạn nên sử dụng NO_ERRORS_SCHEMA. Nó cũng được đề xuất như một cách tiếp cận trong Angular Testing Guide chính thức.

NO_ERRORS_SCHEMA bảo trình biên dịch bỏ qua mọi phần tử hoặc thuộc tính mà nó không quen thuộc. Tôi sẽ sửa ví dụ của tôi để sử dụng NO_ERRORS_SCHEMA:

Thêm NO_ERRORS_SCHEMA chỉ dẫn trình biên dịch bỏ qua ChildComponent. Sau khi làm điều này, test của tôi đang hoạt động trở lại. Mặc dù đây là một giải pháp hợp lệ và được sử dụng rộng rãi, nhưng nó cũng là một giải pháp khiến chúng ta dễ bị sót lỗi, vì nó vi phạm Quy tắc 2: "Thou shalt alert us to broken templates". Điều gì xảy ra nếu chúng ta viết sai thẻ <child> ?

thử nghiệm vẫn thành công. Đây là những gì tôi muốn. Ngay cả khi chúng tôi viết đúng <child>, các test cũng sẽ thành công nếu tôi viết sai chính tả: <child [childs] = "children"> </ child>.

Đây thực sự là cách tiếp cận ban đầu tôi đã thực hiện khi bắt đầu với một dự án Angular. Tuy nhiên, khi dự án tăng kích thước và chúng tôi bắt đầu tái cấu trúc mã, tôi nhanh chóng gặp phải tình huống mà tôi không nhận ra có gì đó sai cho đến khi tôi thực sự chạy ứng dụng. Trong một trường hợp, một thành phần bị hỏng trong một phần được sử dụng không thường xuyên của ứng dụng vẫn bị hỏng trong một vài tuần cho đến khi được phát hiện.

Ngoài ra, cách tiếp cận này cũng khiến cho việc kiểm tra @Input@Output trên các thành phần con rất khó khăn. Hãy xem xét giải pháp khác.

Cách 3: Manually Mocking Components

Tôi sẽ mở rộng ví dụ của mình để thêm đầu vào và đầu ra vào ChildComponent:

Thay vì ChildComponent có tên children được hard-code, tôi đã đặt children làm đầu vào. Tôi cũng đã thêm @input selected: nếu child bằng với selected, tôi hiển thị ❤️ bên cạnh trẻ đó.

Kết quả sẽ có dạng như sau:

Vậy, làm thế nào để tôi kiểm tra ParentComponent ngay bây giờ khi ChildComponent đang làm việc nhiều hơn một chút? Một cách tiếp cận khác, cũng được đề xuất trong Angular Testing Guide, là manually mock (or stub) components. Chúng ta có thể tạo một ChildComponent giả và sử dụng nó thay cho ChildComponent thật. Chúng tôi chỉ cần đảm bảo rằng chúng tôi cung cấp cho nó cùng selector: Bây giờ, trong thiết lập lịch thi đấu, chúng tôi khai báo stub component thay cho thành phần thực:

Điều này hiện cách ly việc thử nghiệm ParentComponent với hoạt động bên trong của ChildComponent. Với mục đích của tôi, tôi muốn coi ChildComponent như một black-box - tôi muốn kiểm tra xem tôi đã chuyển nó đúng đầu vào chưa và chúng tôi xử lý mọi đầu ra mà nó phát ra chính xác. Những gì nó làm với các đầu vào và trong mọi trường hợp nó emits outputs, không liên quan đến ParentComponent (việc kiểm tra logic đó sẽ xảy ra trong các ChildComponent test).

Vì vậy, hãy để test để đảm input và output hoạt động như mong đợi:

Ok.Tôi đã nhanh chóng và dễ dàng kiểm tra chức năng của ParentComponent và các tương tác của nó với ChildComponent. Vậy, có thiếu sót với phương pháp này?

Thiếu sót đầu tiên là nó khá dài dòng. Viết các stub component cho từng component mà tôi muốn mô phỏng có thể gây mệt mỏi rất nhanh, đặc biệt nếu các thành phần có nhiều đầu vào và đầu ra. Ở đây, một ví dụ:

Quay trở lại với ChildComponent. Điều gì sẽ xảy ra nếu chúng ta muốn đổi tên "child" thành một cái gì đó mô tả hơn, như childName?

Có vẻ ổn và tưởng rằng sẽ thành công nếu có ai đó review code của tôi vào nói nên sử dụng cái tên childName sẽ hay hơn? Tôi sẽ đổi code của tôi thành childName vào điều tôi nhận ra là khi thay đổi ở code của tôi thì cái stub component kia cũng phải thay đổi, thạt tệ phải không? Đúng vậy nó thất tệ nếu phải refactor code và phải đổi cả code cả test. Điều đó không nên xảy ra chút nào. Vậy có cách nào tốt hơn không?

Cách 4: Mocking Components using NgMock

Hóa ra, chúng ta có thể làm tốt hơn. Có một thư viện tuyệt vời gọi là ngMocks khiến cho việc tạo ra các stub component(cũng như các directives và pipes) trở nên dễ hơn. Thay vì tự tạo các thành phần, ngMocks tự động tạo các type-safe mocks cho tôi, giảm bớt cả những thiếu sót của cách 3. Trước tiên chúng ta cần cài NgMock

npm install ng-mocks --save-dev

Không còn yêu cầu ChildComponentStub vì vậy hãy xóa nó đi. Tôi sẽ viết lại các unit test của mình để sử dụng ngMocks thay thế. Trước tiên, hãy để thay đổi cấu hình thử nghiệm để tự động mock ChildComponent bằng ngMock’s MockComponent function:

Tôi cũng cần thay đổi chức năng của helper function để trả về ChildComponent thay vì ChildComponentStub:

Bây giờ chúng tôi sẽ nhận được một loạt các lỗi trình biên dịch trong test của tôi Property ‘child’ does not exist on type ‘ChildComponent’ . Vì vậy, đổi tên child thành childName và chạy test lại.

Lúc này test của tôi vẫn đang bị fail.

Failed: Template parse errors: 
Can't bind to 'child' since it isn't a known property of 'child'. 
(" a parent. These are my children:</div> 
<child *ngFor="let child of children" [selected]="selected" 
[ERROR ->][child]="child" (select)="onSelect($event)"></child>")

Khi MockComponent tạo ra một strongly typed mock, chúng tôi sẽ nhận được các lỗi template mà child không tồn tại như là một đầu vào trên ChildComponent, chính xác là những gì chúng ta muốn. Thay vào đó, hãy cập nhật ParentComponent template để sử dụng childName:

Và cứ như thế, tất cả các test của tôi lại pass.

Conclusion

Một demo test sử dụng giải pháp cuối cùng ở đây.

Trong bài viết này, tôi đã làm việc theo cách của tôi từ một giải pháp khá nguyên thủy để test sub components đến một giải pháp khá mạnh mẽ và hiệu quả. Cảm ơn đã theo dõi bài viết của tôi.

Bài viết them khảo tại đây.