Angular IDPStrong Junior
🔥 0

Модуль 8 · урок 3 з 9

TestBed

TestBed створює міні-застосунок Angular для тесту: з інжектором, провайдерами й можливістю рендерити компоненти. Він потужний і повільний — тому головне вміння тут не «як користуватись», а «коли не користуватись».

Коли він не потрібен

// чиста функція — жодного Angular
it('фільтрує за статусом', () => {
  const result = applyState(rows, { status: 'done', page: 1, size: 10, sort: [] });
  expect(result.items.length).toBe(3);
});

// клас без інʼєкцій — просто new
it('додає задачу в стан', () => {
  const store = new TaskStore(apiSpy);
  store.add(task);
  expect(store.snapshot().length).toBe(1);
});

Це найшвидші тести в проєкті: мілісекунди замість сотень мілісекунд. Правило просте — TestBed береш лише тоді, коли без нього неможливо: коли треба рендерити компонент або коли залежності беруться через inject() у контексті інʼєкції.

Мінімальна конфігурація

import { TestBed } from '@angular/core/testing';

describe('TaskStore', () => {
  let store: TaskStore;
  let api: jasmine.SpyObj<TaskApi>;

  beforeEach(() => {
    api = jasmine.createSpyObj<TaskApi>('TaskApi', ['list']);

    TestBed.configureTestingModule({
      providers: [
        TaskStore,
        { provide: TASK_API, useValue: api },   // підміна через токен із модуля 4
      ],
    });

    store = TestBed.inject(TaskStore);
  });
});
Ось за що окупився InjectionToken

Один рядок { provide: TASK_API, useValue: api } — і замість мок-сервісу з випадковими збоями підставляється передбачуваний спай. Якби стор створював API через new, підмінити його було б неможливо, і тест довелось би писати проти справжніх затримок. Це та сама «архітектура», про яку йшлося в модулі 2 — тут вона перетворюється на конкретну економію часу.

Тестування HTTP

import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';

describe('TaskApiHttp', () => {
  let api: TaskApiHttp;
  let http: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [TaskApiHttp, provideHttpClient(), provideHttpClientTesting()],
    });
    api = TestBed.inject(TaskApiHttp);
    http = TestBed.inject(HttpTestingController);
  });

  afterEach(() => http.verify());   // впаде, якщо лишились неопрацьовані запити

  it('запитує список із параметрами', () => {
    let result: Page<Task> | undefined;
    api.list({ page: 2, size: 25 }).subscribe(r => (result = r));

    const req = http.expectOne(r => r.url === '/api/tasks');
    expect(req.request.method).toBe('GET');
    expect(req.request.params.get('page')).toBe('2');

    req.flush({ items: [], total: 0 });   // віддати відповідь

    expect(result?.total).toBe(0);
  });

  it('передає помилку далі', () => {
    let error: unknown;
    api.list({ page: 1, size: 10 }).subscribe({ error: e => (error = e) });

    http.expectOne('/api/tasks').flush('boom', { status: 500, statusText: 'Server Error' });

    expect(error).toBeTruthy();
  });
});

HttpTestingController перехоплює запити й дає віддати відповідь вручну. Жодної мережі, жодних затримок, повний контроль — включно з можливістю змоделювати 500 одним рядком.

Рендер компонента

describe('TaskCard', () => {
  let fixture: ComponentFixture<TaskCard>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [TaskCard],          // standalone-компонент іде в imports
    }).compileComponents();

    fixture = TestBed.createComponent(TaskCard);
  });

  it('показує назву задачі', () => {
    fixture.componentRef.setInput('task', { id: '1', title: 'Зверстати шапку' });
    fixture.detectChanges();        // без цього шаблон не оновиться

    const title = fixture.nativeElement.querySelector('.card__title');
    expect(title.textContent).toContain('Зверстати шапку');
  });
});
setInput, а не присвоєння полю

Для сигнальних input() потрібен саме fixture.componentRef.setInput(). Спроба присвоїти fixture.componentInstance.task = … не спрацює: сигнал вхідного значення доступний лише для читання. І не забувай detectChanges() — Angular у тесті не перевіряє зміни сам.

Чому TestBed повільний

Кожен configureTestingModule створює нове середовище, компілює компоненти й піднімає інжектор. На пʼятдесяти тестах різниця непомітна, на пʼятистах — це хвилини очікування в CI. Звідси й практичне правило: чим більше логіки в чистих функціях, тим швидший набір тестів. Ще один аргумент за поділ container і presentation із модуля 3.

≈ 45 хв · +25 XP за урок