تست کامپوننت ها در Angular: NO ERROR SCHEMA، Stub Components و NgMocks

زمانی که نیاز به نوشتن یک تست برای کامپوننت ها داریم، سخت نیست. درد زمانی شروع می شود که اجزای ما شروع به وابستگی می کنند، نه تنها در سازنده یا در فایل TypeScript. چالش واقعی زمانی پیش میآید که شروع به اضافه کردن وابستگیهای اجزای دیگر میکنیم، و این مؤلفهها وابستگیهای دیگری مانند دستورالعملها، کتابخانههای خارجی یا فرمها دارند.
در آن لحظه است که ما واقعاً شروع به فراخوانی نیروهای تاریک می کنیم تا به ما کمک کنند زنده بمانیم، و شما نمی دانید چه کسی به سؤال شما پاسخ می دهد، اما برخی از راه حل ها به ذهن شما می رسد. NO_ERRORS_SCHEMA
Stub Component و NgMocks
هر گزینه می تواند به شما در حل مشکل کمک کند، اما آیا هزینه پنهان آن را می دانید؟ امروز میخواهیم یاد بگیریم که چگونه اجزاء را با کودکان آزمایش کنیم و چگونه آن موقعیت را مدیریت کنیم. مثل همیشه، بهترین راه برای یادگیری سناریو است.
سناریو
ما یک برنامه Angular داریم که با ماژول ها کار می کند (بله، ما شرکت های زیادی داریم که از ماژول ها در سال 2024 استفاده می کنند). لیستی از محصولات ارائه شده را با استفاده از دو جزء نشان می دهد: ProductComponent
و ProductsListComponent
.
را ProductComponent
استفاده می کند ProductsListComponent
برای نمایش لیست محصولات و آن kendo-viewlist
داخل کار اما بدون آزمایش 🤪
هدف شما افزودن پوشش تست به آن است ProductsComponent
، اما وابستگی های آن را در نظر داشته باشید:
انجام آن سخت به نظر نمی رسد، پس بیایید تست را اجرا کنیم.
در حال اجرا تست ها
ما می دانیم که Angular تست های پیش فرض را برای برنامه ما ایجاد می کند، از آن مراقبت می کند ProductsComponent
، پس بیایید products-component.ts را باز کنیم. پیچیده به نظر نمی رسد
import { Component, inject } from '@angular/core';
import { ProductsService } from '../services/products/products.service';
@Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrl: './products.component.css',
})
export class ProductsComponent {
productService = inject(ProductsService);
total$ = this.productService.totalProductsInOffer;
}
در قالب، چیز زیادی وجود ندارد. کل محصولات را از سرویس دریافت می کند و نشان می دهد ProductListComponent
. کامل.
class="bg-white">
class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8"
>
@if (total$ | async; as totalProducts) {
class="text-2xl font-bold tracking-tight text-gray-900">
We have {{ totalProducts }} in offers
}
تست باید ساده باشد، بیایید آن را باز کنیم products.component.spec.ts
و بررسی کنید که آیا می تواند یک نمونه ایجاد کند. این کار باید در 2 دقیقه حل شود. بیایید اجرا کنیم ng test
.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
describe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixtureProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProductsComponent],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
و… تادا!!! 😭 شروع به دریافت خطاهای عجیب کردم:
NullInjectorError: R3InjectorError(DynamicTestModule)[ProductsService -> HttpClient -> HttpClient]:
چرا NullInjectorError؟
شاید از خود بپرسید که چرا وقتی کامپوننت من کار می کند، خطای انژکتور تهی وجود دارد؟ من خدمات خود را ارائه می دهم، اما ProductsComponent
اکنون در ماسهبازی زندگی میکند TestBedTestingModule
.
Angular اعلام می کند ProductService
برای ما، اما ما آن را ارائه نمی کنیم HttpClientModule
. در عوض، ما از HttpClientTestingModule
پس بیایید آن را وارد کنیم.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixtureProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
تغییرات را ذخیره کنید، همه چیز باید کار کند، بیایید ببینیم 😭
Error: NG0304: 'app-products-list' is not a known element (used in the 'ProductsComponent' component template):
1. If 'app-products-list' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
2. If 'app-products-list' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
یک خطای دیگر گرفتیم! چرا products-component
توضیح دادن در مورد app-products-list
? خوب، ما به آن اشاره می کنیم و Angular می خواهد آن را حل کند. یا سعی کنیم روی تست خود تمرکز کنیم و با استفاده از آن نکته را نادیده بگیریم NO_ERRORS_SCHEMA
.
NO_ERRORS_SCHEMA
😈 ما می خواهیم وظیفه خود را برای آزمایش کامپوننت خود به پایان برسانیم، بنابراین ایده عالی استفاده از آن است NO_ERRORS_SCHEMA
وارد شده از @angular/core
. استفاده كردن NO_ERRORS_SCHEMA
به کامپایلر Angular می گوید که عناصر و ویژگی های ناشناخته را در قالب های شما نادیده بگیرد و آنها را به عنوان عناصر HTML علامت گذاری کند.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixtureProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
ذخیره تغییرات. عالی! همه چیز سبز است!
👀 چرا من NO_ERRORS_SCHEMA را توصیه نمی کنم
وقتی استفاده می کنیم NO_ERRORS_SCHEMA
در تست های Angular ما، خطاهای الگو را پنهان می کند و پوشش تست را کاهش می دهد. این می تواند منجر به مثبت کاذب شود و کیفیت کد ضعیف را ارتقا دهد، که هزینه پنهانی برای پرداخت در آینده است.
🙉 واردات وابستگی های کودک
اگر مشکل به این دلیل است که ProductsListComponent
در اعلام نشده است TestBed
sandbox، چرا آن را وارد نمی کنید؟ بیایید آن را انجام دهیم.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ProductsListComponent } from '../components/products-list/products-list.component';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixtureProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent, ProductsListComponent],
}).compileComponents();
و در اینجا میرویم… وابستگیهای بیشتر و بیشتری را اضافه میکنیم.
Chrome 126.0.0.0 (Mac OS 10.15.7) ProductsComponent should load the product-list FAILED
Error: NG0304: 'kendo-listview' is not a known element (used in the 'ProductsListComponent' component template):
1. If 'kendo-listview' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
f 'kendo-listview' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
error properties: Object({ code: 304 })
.. باشه، بیایید با اضافه کردن آن درستش کنیم ListViewModule
از کندو
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ProductsListComponent } from '../components/products-list/products-list.component';
import { ListViewModule } from '@progress/kendo-angular-listview';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixtureProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule, ListViewModule],
declarations: [ProductsComponent, ProductsListComponent],
}).compileComponents();
یک لحظه صبر کنید… آیا ما وابستگی هایی را به آن اضافه می کنیم ProductsComponent
تستی که به آن مربوط نیست؟ چه اتفاقی خواهد افتاد زمانی که ProductListComponent
تغییر می دهد و یک فرم اضافه می کند؟ خواهد شد ProductComponent
تست و سپس نیاز به FormsModule
? تست ما به دلیل تغییر در آزمون شکسته خواهد شد ProductListComponent
.
به نظر شما این رویکرد خوبی است یا ما یک آزمون واحد را با یک آزمون ادغام مخلوط می کنیم؟
من فکر می کنم ما باید از عمیق شدن بیش از حد در آزمون خود اجتناب کنیم. مسئولیت ما اطمینان از کارکرد محصولات، بازیابی داده ها و نمایش تعداد پیشنهادات است. مسئولیت چیزهای کندو بر عهده لیست محصول است. یک جایگزین این است که در اینجا توقف کنید و آن را مسخره کنید ProductList
.
خرد The Component
ما می خواهیم نسخه واقعی را جایگزین کنیم ProductListComponent
. هنگامی که ما یک خرد ایجاد می کنیم، آن فقط یک کلاس با همان انتخابگر کامپوننت اصلی است و رابط پیاده سازی API را به اشتراک می گذارد. با استفاده از یک خرد، میتوانیم آن را مانند نمای کودک جستجو کنیم، الگو را برای آزمایش خود تنظیم کنیم، و البته از نیاز به همه آن وابستگیها اجتناب کنیم.
ایجاد کلاس با @Component
دکوراتور و الگو را تنظیم کنید. مثل ایجاد یک جزء است. کد به شکل زیر است:
@Component({
selector: 'app-products-list',
template: ` my products
`,
})
export class ProductListStub implements ProductsListComponent {
products: Product[] = [];
}
بعد، جایگزین کنید ProductsListComponent
با خرد ما در بخش اعلامیه. کد نهایی به شکل زیر است:
import { Component } from '@angular/core';
import { Product } from '../services/products/products.service';
@Component({
selector: 'app-products-list',
template: ` my products
`,
})
export class ProductListMock implements ProductsListComponent {
products: Product[] = [];
}
describe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixtureProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent, ProductListStub],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
👀 قیمت پنهان خرد
خوب، چسباندن اجزاء بسیار خوب است، زیرا این یک راه آسان برای جلوگیری از نگرانی در مورد وابستگی مؤلفه فرزند است. البته ما روی تست کنترل داریم، اما هزینه پنهانی برای پرداخت وجود دارد.
ما باید هر جزء را اعلام کنیم و این خرد را حفظ کنیم. اگر روی یک محصول بزرگ کار می کنید، به روز نگه داشتن همه موارد خرد ممکن است تلاش بیشتری باشد که مدیر شما نمی خواهد برای آن هزینه ای بپردازد. بنابراین، بیایید با استفاده از NgMocks راه حل دیگری پیدا کنیم.
از تمسخر
ngMocks
یک کتابخانه قدرتمند است، به ما کمک می کند تا آزمایش را ساده کنیم و آماده سازی آن را آسان کنیم و صفحه دیگ را کاهش دهیم، من عاشق کار با آن هستم.
NgMocks ابزارهای کمکی و کمکی را برای گسترش آزمایش واحد Angular فراهم می کند TestBed
، ComponentFixture
ساده کردنش ما تعداد زیادی صفحه دیگ را با استفاده از ابزارهایی مانند مانند کاهش می دهیم MockBuilder
، MockRender
،MockComponent
، MockDirective
، MockPipe
، MockService
، و AutoMockModule
و بیشتر .
من در مورد تمام جنبه های ngMock عمیق نمی شوم، توصیه می کنم وب سایت را بررسی کنید یا اگر مقاله ای در مورد آن می خواهید نظر بدهید
ابتدا کتابخانه ng-mocks را نصب کنید:
npm install ng-mocks --save-dev
اگر از Angular 15+ استفاده می کنید، ندارید
src/test.ts
از این پاسخ در stackoverflow برای بازیابی استفاده کنیدsrc/test.ts
.
سپس پیکربندی ng-mock را به آن اضافه کنید src/test.ts
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { MockInstance, MockService, ngMocks } from 'ng-mocks';
import { DefaultTitleStrategy, TitleStrategy } from '@angular/router';
import { CommonModule } from '@angular/common';
import { ApplicationModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
{
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
},
);
ngMocks.autoSpy('jasmine');
ngMocks.defaultMock(TitleStrategy, () => MockService(DefaultTitleStrategy));
ngMocks.globalKeep(ApplicationModule, true);
ngMocks.globalKeep(CommonModule, true);
ngMocks.globalKeep(BrowserModule, true);
jasmine.getEnv().addReporter({
specDone: MockInstance.restore,
specStarted: MockInstance.remember,
suiteDone: MockInstance.restore,
suiteStarted: MockInstance.remember,
});
ما می خواهیم تست خود را با استفاده از دو ابزار کلیدی تغییر دهیم: MockBuilder
و MockRender
.
MockBuilder
راه اندازی ماژول های تست زاویه ای را با جایگزینی وابستگی ها ساده می کند و اطمینان می دهد که اجزای تحت آزمایش برای آزمایش متمرکز ایزوله هستند.
MockRender
نمونهسازی و ارائه مولفههای Angular در تستهای واحد را تسهیل میکند و دسترسی به نمونههای مؤلفه و عناصر DOM را برای تأیید رفتار و تعاملات آنها به طور دقیق فراهم میکند.
بجای استفاده از TestBed
، من به MockBuilder سوئیچ می کنم و مؤلفه را برای آزمایش و ماژول مورد نیاز را در مورد من پاس می کنم HttpClientTestingModule
، و در نهایت تماس بگیرید .mock
برای تعیین اینکه کدام جزء را می خواهم مسخره کنم، ProductList
.
متد .mock از آرایه ای مانند .mock([ProductsListComponent, AnotherComponent, …])
کد نهایی به نظر می رسد:
import { ProductsComponent } from './products.component';
import { ProductsListComponent } from '../components/products-list/products-list.component';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('ProductsComponent', () => {
beforeEach(() =>
MockBuilder(ProductsComponent)
.keep(HttpClientTestingModule)
.mock(ProductsListComponent),
);
it('should create', () => {
const fixture = MockRender(ProductsComponent);
expect(ngMocks.findInstance(ProductsComponent)).toBeTruthy();
});
});
ذخیره و تا-دا! ما فقط با چند خط کد آماده هستیم و آماده شروع آزمایش هستیم!
خلاصه
ما در مورد چگونگی پیچیده شدن مولفه های Angular زمانی که وابستگی هایی مانند کتابخانه های خارجی، مؤلفه های فرزند درگیر می شوند، پیچیده می شوند. در ابتدا ساده است، اما هنگام چالش با مؤلفههایی که به چند وابستگی متکی هستند، برای مثال در قالب، سخت میشود.
وقتی استفاده می کنیم NO_ERRORS_SCHEMA
، در ابتدا مسائل را پنهان می کند. ایجاد مولفه های خرد آزمایش را ساده می کند، اما هزینه تعمیر و نگهداری را اضافه می کند. به نظر من ترجیح می دهم NgMocks
زیرا کار من را با ابزارهایی مانند ساده می کند MockBuilder
و MockRender
و حداقل دیگ بخار.
امیدوارم این مقاله کمک کند