برنامه نویسی

تست کامپوننت ها در 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();
  });
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ذخیره تغییرات. عالی! همه چیز سبز است!

5

👀 چرا من 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 و حداقل دیگ بخار.

امیدوارم این مقاله کمک کند

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا