Тестирование форм

Формы в Angular — один из самых насыщенных логикой элементов приложения. Они объединяют шаблоны, модели данных, валидацию, асинхронные операции и пользовательские события. Ошибка в форме почти всегда напрямую влияет на пользовательский опыт и корректность данных. Поэтому тестирование форм рассматривается как обязательная часть разработки, а не дополнительная мера.

Angular предоставляет встроенные инструменты для работы с формами и их тестирования, опираясь на Jasmine, Karma и TestBed. Подходы различаются в зависимости от типа формы: template-driven forms и reactive forms.


Подготовка окружения для тестирования форм

Тестирование форм выполняется в стандартной тестовой среде Angular:

  • TestBed — конфигурация тестового модуля
  • ComponentFixture — доступ к экземпляру компонента и DOM
  • DebugElement — работа с элементами шаблона
  • FormsModule и ReactiveFormsModule — обязательны для тестирования форм

Пример базовой конфигурации:

beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [MyFormComponent],
    imports: [ReactiveFormsModule]
  }).compileComponents();
});

Отсутствие нужного модуля приводит к ошибкам вида No value accessor for form control.


Тестирование Reactive Forms

Reactive Forms строятся на основе FormGroup, FormControl и FormArray, что делает их особенно удобными для юнит-тестирования.

Проверка инициализации формы

Первый уровень тестирования — корректное создание структуры формы:

it('должна создаваться форма с нужными контролами', () => {
  expect(component.form.contains('email')).toBeTrue();
  expect(component.form.contains('password')).toBeTrue();
});

Важно проверять:

  • наличие контролов
  • начальные значения
  • наличие валидаторов

Тестирование значений контролов

it('должен принимать email', () => {
  const control = component.form.get('email');
  control?.setValue('test@example.com');
  expect(control?.value).toBe('test@example.com');
});

Проверка значений выполняется без взаимодействия с DOM, что ускоряет тесты и снижает их хрупкость.


Тестирование валидаторов

it('email должен быть невалидным при пустом значении', () => {
  const control = component.form.get('email');
  control?.setValue('');
  expect(control?.valid).toBeFalse();
});

Для кастомных валидаторов проверяется:

  • результат errors
  • конкретные ключи ошибок
expect(control?.errors?.['required']).toBeTrue();

Тестирование состояния формы

Angular формы имеют состояния:

  • valid / invalid
  • touched / untouched
  • dirty / pristine
it('форма должна быть dirty после изменения', () => {
  component.form.get('email')?.setValue('a@a.ru');
  expect(component.form.dirty).toBeTrue();
});

Тестирование Template-driven Forms

Template-driven формы зависят от шаблона и директив ngModel, поэтому тестирование чаще выполняется через DOM.

Подключение FormsModule

imports: [FormsModule]

Без этого директивы формы не будут работать.


Доступ к форме через ViewChild

@ViewChild('form') form!: NgForm;

Тестирование выполняется после fixture.detectChanges().


Проверка валидности формы

it('форма должна быть невалидной при пустых полях', () => {
  expect(component.form.valid).toBeFalse();
});

Изменение значений через DOM

const input: HTMLInputElement = fixture.nativeElement.querySelector('input[name="email"]');
input.value = 'test@test.ru';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();

После этого Angular обновляет модель формы.


Тестирование отображения ошибок в шаблоне

Форма считается протестированной не полностью, если не проверено отображение ошибок пользователю.

it('должно отображаться сообщение об ошибке', () => {
  const control = component.form.get('email');
  control?.setValue('');
  control?.markAsTouched();
  fixture.detectChanges();

  const error = fixture.nativeElement.querySelector('.error');
  expect(error).toBeTruthy();
});

Ключевые моменты:

  • установка touched
  • вызов detectChanges
  • поиск элемента в DOM

Тестирование submit-логики

Отправка формы почти всегда связана с сервисами.

it('должен вызываться метод submit', () => {
  spyOn(component, 'onSubmit');

  component.form.setValue({
    email: 'test@test.ru',
    password: '123456'
  });

  fixture.nativeElement.querySelector('form').dispatchEvent(new Event('submit'));
  expect(component.onSubmit).toHaveBeenCalled();
});

Мокаем сервисы при тестировании форм

Формы часто отправляют данные на сервер. Используется spyOn или подмена сервиса:

const authService = TestBed.inject(AuthService);
spyOn(authService, 'login').and.returnValue(of(true));

Проверка вызова:

expect(authService.login).toHaveBeenCalledWith({
  email: 'test@test.ru',
  password: '123456'
});

Асинхронные валидаторы

Асинхронные валидаторы требуют fakeAsync и tick.

it('должен быть невалиден при занятом email', fakeAsync(() => {
  const control = component.form.get('email');
  control?.setValue('taken@mail.ru');
  tick(500);
  expect(control?.valid).toBeFalse();
}));

Тестирование FormArray

it('должен добавляться элемент в массив', () => {
  const array = component.form.get('phones') as FormArray;
  component.addPhone();
  expect(array.length).toBe(1);
});

Также проверяется:

  • удаление элементов
  • валидность каждого элемента
  • динамическое обновление шаблона

Лучшие практики тестирования форм

Изоляция логики

  • Проверка валидаторов без DOM
  • Минимизация fixture.detectChanges()

Читаемость тестов

  • Один тест — одно ожидание
  • Ясные имена тестов

Надёжность

  • Минимум селекторов по CSS-классам
  • Предпочтение form.get() вместо querySelector

Производительность

  • Использование shallow testing, если шаблон сложный
  • Вынос кастомных валидаторов в отдельные функции

Частые ошибки

  • Отсутствие нужного модуля формы в тесте
  • Проверка DOM без detectChanges
  • Тестирование шаблона вместо логики
  • Игнорирование состояний touched и dirty

Итоговая структура тестов формы

  • Инициализация формы
  • Проверка структуры
  • Проверка валидаторов
  • Проверка состояний
  • Проверка шаблона
  • Проверка submit-логики
  • Проверка взаимодействия с сервисами

Тестирование форм в Angular — это сочетание юнит-тестов и DOM-проверок, где основная логика должна оставаться максимально изолированной и предсказуемой.