Angular IDPStrong Junior
Модулі / Юніт-тести / Тест валідатора
🔥 0

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

Тест валідатора

Останні два тести з пʼяти. Синхронний валідатор тестується тривіально — це просто функція. Асинхронний цікавіший: там треба і час, і підмінений сервіс, і розуміння станів контролю.

Синхронний валідатор як функція

describe('taskCode', () => {
  const validate = taskCode();

  function check(value: string) {
    return validate({ value } as AbstractControl);   // достатньо обʼєкта з полем value
  }

  it('пропускає коректний код', () => {
    expect(check('KYIV-104')).toBeNull();
  });

  it('не чіпає порожнє значення', () => {
    expect(check('')).toBeNull();     // це справа required
  });

  it('ловить малі літери', () => {
    expect(check('kyiv-104')).toEqual({ taskCode: { actual: 'kyiv-104' } });
  });

  it('ловить відсутність дефіса', () => {
    expect(check('KYIV104')).toBeTruthy();
  });
});
Не треба справжнього FormControl

Валідатор читає лише control.value, тож достатньо передати { value } as AbstractControl. Створювати повноцінний контрол — зайва робота, яка нічого не додає до перевірки. Це той випадок, коли приведення типу виправдане: воно локальне, у тесті, і не приховує реальної проблеми.

Тест валідатора на рівні групи

describe('dateRange', () => {
  const validate = dateRange('from', 'to');

  function group(from: string, to: string): AbstractControl {
    return new FormGroup({
      from: new FormControl(from),
      to: new FormControl(to),
    });
  }

  it('пропускає коректний діапазон', () => {
    expect(validate(group('2026-01-01', '2026-02-01'))).toBeNull();
  });

  it('ловить зворотний діапазон', () => {
    expect(validate(group('2026-03-01', '2026-02-01'))).toEqual({ dateRange: true });
  });

  it('не чіпає неповні дані', () => {
    expect(validate(group('2026-01-01', ''))).toBeNull();
  });
});

Тут справжня FormGroup потрібна, бо валідатор шукає дітей через group.get(...). Але TestBed усе одно не потрібен: FormGroup — звичайний клас, який створюється через new.

Асинхронний валідатор

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

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

  it('повертає помилку для зайнятого коду', fakeAsync(() => {
    api.isCodeTaken.and.returnValue(of(true));
    const validate = uniqueCode(api);

    let result: ValidationErrors | null = null;
    (validate({ value: 'KYIV-1' } as AbstractControl) as Observable<ValidationErrors | null>)
      .subscribe(r => (result = r));

    tick(400);   // промотати внутрішній debounce

    expect(result).toEqual({ codeTaken: true });
    expect(api.isCodeTaken).toHaveBeenCalledWith('KYIV-1');
  }));

  it('не питає сервер про поточний код', fakeAsync(() => {
    const validate = uniqueCode(api, 'KYIV-1');   // редагування наявного запису

    let result: ValidationErrors | null = { any: true };
    (validate({ value: 'KYIV-1' } as AbstractControl) as Observable<ValidationErrors | null>)
      .subscribe(r => (result = r));

    tick(400);

    expect(result).toBeNull();
    expect(api.isCodeTaken).not.toHaveBeenCalled();   // зайвого запиту не було
  }));

  it('не блокує форму при збої сервера', fakeAsync(() => {
    api.isCodeTaken.and.returnValue(throwError(() => new Error('boom')));
    const validate = uniqueCode(api);

    let result: ValidationErrors | null = { any: true };
    (validate({ value: 'KYIV-9' } as AbstractControl) as Observable<ValidationErrors | null>)
      .subscribe(r => (result = r));

    tick(400);

    expect(result).toBeNull();   // catchError відпрацював
  }));
});
Три тести — три різні речі

Перший перевіряє основну поведінку. Другий — оптимізацію, про яку легко забути при рефакторингу (виняток для поточного коду). Третій — стійкість до збою. Разом вони фіксують усі чотири деталі з уроку модуля 6, і якщо хтось прибере catchError, тест одразу скаже, що саме зламалось.

Тест форми цілком

it('форма невалідна без обовʼязкових полів', () => {
  const form = createTaskForm();          // фабрика форми, винесена з компонента

  expect(form.valid).toBe(false);

  form.patchValue({ code: 'KYIV-1', title: 'Назва' });

  expect(form.valid).toBe(true);
});

Якщо створення форми винести у фабричну функцію, її можна тестувати без компонента взагалі. Це маленький рефакторинг із великою віддачею: перевірка структури форми стає такою ж дешевою, як тест чистої функції.