Модуль 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();
});
});
Валідатор читає лише 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);
});
Якщо створення форми винести у фабричну функцію, її можна тестувати без компонента взагалі. Це маленький рефакторинг із великою віддачею: перевірка структури форми стає такою ж дешевою, як тест чистої функції.