برنامه نویسی

مشکلات رایج در تست اجزای زاویه ای

وقتی در مورد تست واحد صحبت می کنیم، اولین تعریفی که به ذهن ما می رسد این است که بخش های کوچک قابل آزمایش یک برنامه مانند توابع، متدها و کلاس ها را آزمایش کنیم. برای یک تابع ساده یا کلاس ایزوله بدون وابستگی عالی به نظر می رسد، اما اینطور است نه در دنیای واقعی.

در انگولار کامپوننت ها دارای دکوراتورها، سرویس ها، لوله ها، قالب هایی برای رندر اطلاعات هستند، و گاهی اوقات کامپوننت های فرزند، بنابراین ما بازیگران زیادی در آزمون خود داریم.

این مقاله بر مشکلات یا مسائل معمولی تمرکز می‌کند که وقتی شروع به اضافه کردن آزمایش در یک مؤلفه با وابستگی‌هایی مانند خدمات و مؤلفه‌های فرزند می‌کنیم، با آنها مواجه می‌شویم.

سناریو

ما یک برنامه داریم که لیستی از بازیکنان NBA را از API با استفاده از آن نشان می دهد PlayerService; این برنامه با دو جزء اصلی کار می کند.

  • پخش کننده: جزء گنگ برای نمایش اطلاعات تک نفره.

  • PlayerListComponent: از لیست بازیکنان استفاده می کند PlayerService\ برای رندر در نما با استفاده از مؤلفه پخش کننده.

ما قصد داریم آن را آزمایش کنیم playerlist.component.ts رسیدگی به وابستگی های خدمات و مؤلفه فرزند.

تایپ اسکریپت:

import { Component, OnInit } from '@angular/core';
import { PlayerService } from 'src/app/services/players.services';
import { APIResponse, Player } from '../models/player';
@Component({
  selector: 'app-player-list',
  templateUrl: './player-list.component.html',
  styleUrls: ['./player-list.component.css'],
})
export class PlayerListComponent implements OnInit {

  players: Player[] = [];

  constructor(private playerService: PlayerService) { }

  ngOnInit(): void {
    this.playerService.getPlayers().subscribe((players: APIResponse) => {
      this.players = players.data;
    });
  }
}
وارد حالت تمام صفحه شوید

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

نشانه گذاری HTML از <app-player> جزء.

  <div *ngFor="let player of players" >
    <app-player [player]="player"></app-player>
  </div>
وارد حالت تمام صفحه شوید

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

بیایید سعی کنیم کامپوننت را تست کنیم!

پایه یاس

قبل از شروع، به یاد داشته باشید که Angular به Jasmine و Karma به عنوان دونده آزمون و تست های ما متکی است. اگر هرگز تستی نمی نویسید، کوتاه و مقدمه است.

  1. describe : تست های مرتبط با گروه با هم. یک رشته و یک تابع را به عنوان آرگومان می گیرد.

  2. beforeEach : قبل از هر تست برای عملیات راه اندازی اجرا می شود.

  3. it : یک مورد آزمایشی را تعریف می کند. یک رشته و یک تابع را به عنوان آرگومان می گیرد.

  4. expect : برای اثبات رفتار کد مورد آزمایش استفاده می شود.

درباره یاس و کارما بیشتر بدانید

Testbed چیست

بستر آزمایشی یک محیط تست قدرتمند و با کاربری آسان است که به ما امکان می‌دهد اجزا و سرویس‌ها را به صورت مجزا و بدون وابستگی به بقیه برنامه‌ها آزمایش کنیم. مجموعه ای از API ها را برای ایجاد و پیکربندی محیط آزمایش و تعامل با مؤلفه یا سرویس آزمایش شده ارائه می دهد.

با استفاده از بستر آزمایش، از دو روش استفاده می کنیم: configureTestingModule و createComponent .

برای پیکربندی یک تخت تست برای یک ماژول یا جزء خاص، از آن استفاده می کنیم configureTestingModule() متد یک شی را به عنوان آرگومان برای پیکربندی ارائه‌دهندگان، اعلان‌ها، واردات و سایر گزینه‌های مشابه می‌گیرد. app.module.ts.

بیشتر بدانید بستر آزمایش

این createComponent نمونه ای از یک جزء ایجاد می کند و a را برمی گرداند ComponentFixture برای آن مؤلفه، امکان آزمایش رفتار مؤلفه و تعاملات DOM را فراهم می کند.

ما ایجاد می کنیم player-list.component.spec.ts ، اضافه کردن describe با عنوان آزمون

describe('PlayerList Component', () => {

})
وارد حالت تمام صفحه شوید

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

متغیر ثابت را برای ذخیره کامپوننت با استفاده از آن اعلام کنید ComponentFixture ، و متغیر را با استفاده از نوع تنظیم کنید PlayerListComponent.

describe('PlayerList Component', () => {

  let fixture: ComponentFixture<PlayerListComponent>

  });
وارد حالت تمام صفحه شوید

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

زمان استفاده از BeforeEach و TestBed برای پیکربندی ماژول تست ما، همانطور که قبل از آن یاد گرفتیم configureTestingModule به ما در راه اندازی ماژول کمک کنید.

را اعلام کنید PlayerListComponent و با TestBed.createComponent برای ایجاد نمونه از PlayerListComponent.

کد نهایی به شکل زیر است:


import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlayerListComponent } from './player-list.component';
describe('PlayerList Component', () => {

  let fixture: ComponentFixture<PlayerListComponent>

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [PlayerListComponent]
    })
    fixture = TestBed.createComponent(PlayerListComponent);
  })
  it('should render the component', () => {
    expect(true).toBe(true)
  })
})
وارد حالت تمام صفحه شوید

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

تغییرات را ذخیره کنید و دستور را اجرا کنید: npm run test:

✔ Browser application bundle generation complete.
Chrome 108.0.0.0 (Windows 10) PlayerList Component should render the component FAILED
        NullInjectorError: R3InjectorError(DynamicTestModule)[PlayerService -> PlayerService]: 
          NullInjectorError: No provider for PlayerService!
        error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'PlayerService', 'PlayerService' ] })
Chrome 108.0.0.0 (Windows 10): Executed 1 of 1 (1 FAILED) (0.046 secs / 0.04 secs)
TOTAL: 1 FAILED, 0 SUCCESS
وارد حالت تمام صفحه شوید

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

ما یک خطا دریافت کردیم زیرا کامپوننت از آن استفاده می کند PlayerService در سازنده، بنابراین باید آن را ارائه کنیم PlayerService بدون درخواست واقعی

با استفاده از تابع، ما را نجات دهید createSpyObj از jasmine، ما می‌توانیم اشیاء را با روش‌های جاسوسی مسخره کنیم و به شما این امکان را می‌دهد تا رفتار عملکردهای خاص را بدون اجرای واقعی ردیابی و آزمایش کنید.

یک متغیر جدید اعلام کنید mockPlayerService استفاده كردن jasmine.createSpyObj ، مجبور شد روش ها را به Mock اعلام کند. در مورد ما، getPlayers


import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlayerService } from 'src/app/services/players.services';
import { PlayerListComponent } from './player-list.component';
describe('PlayerList Component', () => {

  let fixture: ComponentFixture<PlayerListComponent>

  const mockPlayerService = jasmine.createSpyObj<PlayerService>(["getPlayers"]);
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [PlayerListComponent],
      providers: [{
        provide: PlayerService,
        useValue: mockPlayerService
      }]
    })
    fixture = TestBed.createComponent(PlayerListComponent);
  })
  it('should render the component', () => {
    expect(true).toBe(true)
  })
})
وارد حالت تمام صفحه شوید

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

همه چیز سبز است! بنابراین می توانیم شروع به نوشتن تست خود کنیم.

بیشتر بدانید مسخره و جاسوس

تست کامپوننت

ما ماژول تست را آماده داریم. ما برای کارهای زیر آماده هستیم تا با انجام کارهای زیر کامپوننت را تست کنیم.

  • [x] جعل داده های پاسخ

  • [x] یک تست برای مؤلفه دریافت بازیکنان از سرویس ایجاد کنید

  • [x] مولفه را فعال کنید change-detection.

  • [x] دارایی را اعتبار سنجی کنید players دارای ارزش از پاسخ جعلی است.

اجازه دهید یک تست برای جذب بازیکنان ایجاد کنید از خدمات ما یک متغیر با ساختار از backend ایجاد می کنیم تا از آن به عنوان پاسخ داده جعلی استفاده کنیم:


  const FAKE_API_RESPONSE: APIResponse = {
    data: [{
      id: 1,
      first_name: 'Lebron'
    }]
  };
وارد حالت تمام صفحه شوید

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

یک تست برای دریافت بازیکنان از سرویس ایجاد کنید با it باید بازیکنان را از خدمات و استفاده از آنها دریافت کند and.returnValue با ساختگی برای بازگشت قابل مشاهده به عنوان یک پاسخ جعلی، باید تشخیص تغییر را فعال کنید استفاده كردن fixture.detectChanges برای شروع چرخه حیات تشخیص تغییر در کامپوننت.

  it('should get players from service', () => {
    mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
    fixture.detectChanges();
  })
وارد حالت تمام صفحه شوید

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

ذخیره و … DAMMMM!!! یک خطای جدید گرفتیم!!!

PlayerList Component > should get players from the service
Error: NG0304: 'app-player' is not a known element (used in the 'PlayerListComponent' component template):
1. If 'app-player' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
2. If 'app-player' is a Web Component, then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
وارد حالت تمام صفحه شوید

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

چرا خطا: NG0304: ‘app-player’ یک عنصر شناخته شده نیست؟

لیست پخش از مؤلفه ای استفاده می کند که در ماژول آزمایشی ما ثبت نشده است. چه گزینه ای دارم؟

این schemas ملک در @NgModule دکوراتور اعتبار الگو را با استفاده از NO_ERRORS_SCHEMA ثابت که اعتبار سنجی الگو را به طور کامل غیرفعال می کند.

بیایید موارد زیر را امتحان کنیم:


....CODE COLLAPSED
TestBed.configureTestingModule({
      declarations: [PlayerListComponent],
      providers: [
        {
          provide: PlayerService,
          useValue: mockPlayerService,
        },
      ],
      schemas: [NO_ERRORS_SCHEMA],
    });
    fixture = TestBed.createComponent(PlayerListComponent);
  });
  it('should get players from service', () => {
    mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
    fixture.detectChanges();
  });
});
وارد حالت تمام صفحه شوید

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

توجه: رویکردهای بهتری نسبت به نادیده گرفتن مسائل قالب وجود دارد. بعداً راه دیگری برای رسیدگی به آنها پیدا خواهیم کرد.

ذخیره، و … آره، دوباره سبز. بیایید به اعتبارسنجی ارزش دارایی از پاسخ جعلی ادامه دهیم.

چگونه مطمئن شویم که بازیکن دارایی ارزش دارد؟ فیکسچر چند راه برای دسترسی به مؤلفه را نشان می دهد:

  1. componentInstance: نمونه ای از مؤلفه آزمایش شده را برمی گرداند. ما می توانیم از این ویژگی برای دسترسی به ویژگی ها و متدهای کامپوننت و بررسی مقادیر آنها استفاده کنیم.

  2. debugElement: نمونه ای از DebugElement جزء تست شده این DebugElement یک پوشش در اطراف عنصر بومی است و به شما امکان می دهد با DOM کامپوننت به گونه ای تعامل داشته باشید که برای پلتفرم زیربنایی آگنوستیک باشد.

  3. debugElement.nativeElement: عنصر اصلی مؤلفه آزمایش شده، عنصر واقعی در DOM را برمی گرداند. می‌توانیم از این ویژگی برای تعامل با DOM مؤلفه به‌طور غیرمستقیم استفاده کنیم و ویژگی‌ها، سبک‌ها و محتوای آن را بررسی کنیم.

بیایید استفاده کنیم componentInstance برای دسترسی به ویژگی بازیکنان، با استفاده از expect برای اثبات مقادیر، FAKE_API_REPONSE، یک آیتم را برگردانید و بررسی کنید که طول بازیکنان برابر با 1 باشد.

 it('should get players from service', () => {
    mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
    fixture.detectChanges();
    expect(fixture.componentInstance.players.length).toEqual(1)
  });
وارد حالت تمام صفحه شوید

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

ذخیره و نتایج:

cae0091b 1245 4c6f 8aab 5317d11670d9

کار با اجزای کودک

یادتان هست که الگو را نادیده گرفتیم؟ آن راه حل فقط یک پچ بود. اگر می‌خواهیم مطمئن شویم که کامپوننت ما بازیکنان را رندر می‌کند، باید تأیید کنیم که الگو کار می‌کند.

مشابه کاری که با این سرویس انجام دادیم، می توانیم آن را مسخره کنیم <app-player> جزء، بنابراین حذف کنید schemas: [NO_ERRORS_SCHEMA] .

برای مسخره کردن کامپوننت، یک کامپوننت جدید را در تست با همان انتخابگر اعلام می کنیم app-player و خواص مورد نیاز آزمایش ما. در قالب، کلاس CSS را اضافه کنید player تا پیدا کردن عناصر آسان شود.

 @Component({
    selector: 'app-player',
    template: `<div class="player">
     <span>{{player.name}}</span>
   </div>`
  })
  class MockPlayer {
    @Input() player!: Player;
  }
وارد حالت تمام صفحه شوید

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

بعد، ثبت نام کنید MockPlayer در بخش اعلامیه ها:

 TestBed.configureTestingModule({
      declarations: [PlayerListComponent, MockPlayer],
      providers: [
        {
          provide: PlayerService,
          useValue: mockPlayerService,
        },
      ]
    });
وارد حالت تمام صفحه شوید

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

چه کار کردیم؟

ما یک مؤلفه Mock را با همان انتخابگر ارائه می دهیم و در ماژول تست ثبت می کنیم. هنگامی که کامپوننت را درخواست می کند، ماژول تست ماژول ما را ارائه می دهد.

کامپوننت های Child Render را تست کنید

ابتدا، مشابه آزمایش دیگر ما، باید داده های جعلی را اختصاص دهیم و تشخیص تغییر را آغاز کنیم.

برای یافتن و تعامل با کامپوننت، به چند روش از فیکسچر کامپوننت می پردازیم.

queryAll آرایه ای از DebugElements را برمی گرداند که با یک گزاره داده شده مطابقت دارند.

by.css یک محمول queryAll برای یافتن عناصری که با یک انتخابگر CSS مطابقت دارند.

by.directive به عنوان محمول استفاده کنید queryAll برای پیدا کردن عناصری که با یک دستورالعمل مشخص شده اند.

این debugElement یک شی است که دسترسی به عنصر اصلی و نمونه مؤلفه مرتبط با یک عنصر DOM داده شده در یک فیکسچر آزمایشی را فراهم می کند.

از آنجا که ما یک کلاس CSS را در مؤلفه ساختگی اضافه می کنیم، از By.css برای پرس و جوی همه عناصر با کلاس استفاده می کنیم. div.player رندر در DOM

یک متغیر جدید ایجاد کنید totalPlayers و نتیجه را ذخیره کنید queryAll که در totalPlayer آرایه ای را برمی گرداند، از totalPlayer.length انتظار آزمون

  it('should render the players', () => {
    mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
    fixture.detectChanges();
const totalPlayers =      fixture.debugElement.queryAll(By.css('div.player')
    expect(totalPlayers).length).toBe(1);
  })
وارد حالت تمام صفحه شوید

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

ذخیره کنید، و آزمایشات ما با استفاده از یک سرویس و الگوی ساختگی قبول می شوند.

22868825 6e4e 4aa6 b426 9c73320600bd

توجه داشته باشید: جایگزین دیگر با By.directive با استفاده از نام کلاس کامپوننت

expect(fixture.debugElement.queryAll(By.directive(MockPlayer)).length).toBe(1);
وارد حالت تمام صفحه شوید

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

انجام شده! ما قبلاً مؤلفه خود را آزمایش کرده ایم:

  • [x] ما مؤلفه را تأیید می کنیم تا داده ها را دریافت کنیم.

  • [x] ما تأیید می کنیم که تاریخ در DOM است.

  • [x] ما یاد می گیریم که سرویس ها و کامپوننت ها را تقلید کنیم

  • [x] ما یاد می گیریم که عناصر را در DOM با استفاده از By پرس و جو کنیم

نتیجه

هنگام تست کامپوننت ها در Angular با وابستگی های متعدد، مشکلات رایج را یاد می گیریم. تست مولفه‌ها با بستر تست، سرویس‌های تمسخرآمیز و اجزای فرزند بخش مهمی از اطمینان از استحکام و قابلیت اطمینان برنامه شما است.

سرویس‌های مسخره و مؤلفه‌های فرزند شما را قادر می‌سازد که ورودی و خروجی آن وابستگی‌ها را کنترل کنید و آزمایش مولفه را به صورت مجزا آسان‌تر می‌کند.

این تکنیک ها می توانند استراتژی تست شما را بهبود بخشند و برنامه های قابل اعتمادتر و قابل نگهداری بیشتری بسازند.

اگر دوست داشتید لطفا به اشتراک بگذارید 🙂

عکس لویی رید در Unsplash

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

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

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

همچنین ببینید
بستن
دکمه بازگشت به بالا