45. Тестирование: Jasmine/Karma
🧪 Тестирование Angular: Jasmine/Karma
Заголовок раздела «🧪 Тестирование Angular: Jasmine/Karma»Тестирование — это страховой полис твоего кода. Angular поставляется с настроенным Jasmine (фреймворк) + Karma (runner) из коробки. Давай разберём всё: от TestBed до async тестов 🛡️
Структура теста в Jasmine
Заголовок раздела «Структура теста в Jasmine»describe('UsersService', () => { // Группа тестов (suite) let service: UsersService;
beforeEach(() => { // Выполняется перед каждым it() TestBed.configureTestingModule({ providers: [UsersService] }); service = TestBed.inject(UsersService); });
afterEach(() => { // После каждого теста // Очистка });
it('should be created', () => { // Конкретный тест (spec) expect(service).toBeTruthy(); // Assertion (проверка) });});TestBed — конфигурация тестового модуля
Заголовок раздела «TestBed — конфигурация тестового модуля»import { TestBed } from '@angular/core/testing';import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';import { RouterTestingModule } from '@angular/router/testing';
describe('UserService', () => { let service: UserService; let httpMock: HttpTestingController;
beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, // Мокает HttpClient RouterTestingModule, // Мокает Router ], providers: [ UserService, // Замена реального сервиса моком { provide: AuthService, useValue: { isLoggedIn: () => true } }, // Или SpyObj: { provide: NotificationService, useValue: jasmine.createSpyObj('NotificationService', ['showError', 'showSuccess']) }, ] });
service = TestBed.inject(UserService); httpMock = TestBed.inject(HttpTestingController); });
afterEach(() => { httpMock.verify(); // Убеждаемся, что нет необработанных HTTP запросов });});Тестирование сервисов
Заголовок раздела «Тестирование сервисов»describe('UserService', () => { let service: UserService; let httpMock: HttpTestingController;
beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [UserService] }); service = TestBed.inject(UserService); httpMock = TestBed.inject(HttpTestingController); });
afterEach(() => httpMock.verify());
it('should fetch users', () => { const mockUsers = [{ id: '1', name: 'Яша' }];
// Подписываемся service.getAll().subscribe(users => { expect(users).toEqual(mockUsers); expect(users.length).toBe(1); });
// Эмулируем HTTP-ответ const req = httpMock.expectOne('/api/users'); expect(req.request.method).toBe('GET'); req.flush(mockUsers); });
it('should handle HTTP error', () => { service.getAll().subscribe({ next: () => fail('Expected error'), error: (error) => { expect(error.status).toBe(500); } });
const req = httpMock.expectOne('/api/users'); req.flush('Server error', { status: 500, statusText: 'Internal Server Error' }); });
it('should create user', () => { const createdUser = { id: '10', ...newUser };
service.create(newUser).subscribe(user => { expect(user.id).toBe('10'); });
const req = httpMock.expectOne('/api/users'); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual(newUser); req.flush(createdUser); });});ComponentFixture — тестирование компонентов
Заголовок раздела «ComponentFixture — тестирование компонентов»import { ComponentFixture, TestBed } from '@angular/core/testing';import { By } from '@angular/platform-browser';import { DebugElement } from '@angular/core';
describe('UserCardComponent', () => { let component: UserCardComponent; let fixture: ComponentFixture<UserCardComponent>; let debugEl: DebugElement;
const mockUser: User = { id: '1', name: 'Яша', isActive: true, };
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserCardComponent] // Standalone компонент }).compileComponents();
fixture = TestBed.createComponent(UserCardComponent); component = fixture.componentInstance; debugEl = fixture.debugElement;
// Передаём входные данные component.user = mockUser; fixture.detectChanges(); // Запускаем change detection });
it('should display user name', () => { const nameEl = debugEl.query(By.css('[data-testid="user-name"]')); expect(nameEl.nativeElement.textContent).toContain('Яша'); });
it('should emit delete event on button click', () => { spyOn(component.delete, 'emit');
const deleteBtn = debugEl.query(By.css('[data-testid="delete-btn"]')); deleteBtn.nativeElement.click();
expect(component.delete.emit).toHaveBeenCalledWith(mockUser.id); });
it('should show active badge when user is active', () => { const badge = debugEl.query(By.css('.active-badge')); expect(badge).toBeTruthy(); });
it('should hide active badge when user is inactive', () => { component.user = { ...mockUser, isActive: false }; fixture.detectChanges();
const badge = debugEl.query(By.css('.active-badge')); expect(badge).toBeNull(); });});async/fakeAsync/tick
Заголовок раздела «async/fakeAsync/tick»describe('Async tests', () => { it('should work with async/await', async () => { const result = await someAsyncFunction(); expect(result).toBe('expected'); });
it('should work with fakeAsync + tick', fakeAsync(() => { let result = '';
// Имитируем вызов с задержкой setTimeout(() => { result = 'done'; }, 1000);
expect(result).toBe(''); // Ещё пусто
tick(1000); // Перематываем время на 1 секунду
expect(result).toBe('done'); // Теперь есть }));
it('should test observables', fakeAsync(() => { let value = '';
of('hello').pipe(delay(500)).subscribe(v => value = v); expect(value).toBe('');
tick(500); expect(value).toBe('hello'); }));
it('should use waitForAsync for template-heavy tests', waitForAsync(() => { fixture.detectChanges();
fixture.whenStable().then(() => { expect(component.data).toBeTruthy(); }); }));});spyOn — мокирование методов
Заголовок раздела «spyOn — мокирование методов»it('should call notification service on error', () => { const notificationService = TestBed.inject(NotificationService);
// Создаём шпиона const spy = spyOn(notificationService, 'showError');
// Или spyOn с returnValue: spyOn(userService, 'getAll').and.returnValue( throwError(() => new Error('Network error')) );
// Или с callFake: spyOn(userService, 'getAll').and.callFake(() => of([{ id: '1', name: 'Mock' }]) );
component.ngOnInit();
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith('Network error'); expect(spy).toHaveBeenCalledTimes(1);});Тестирование Pipes
Заголовок раздела «Тестирование Pipes»describe('DateFormatPipe', () => { let pipe: DateFormatPipe;
beforeEach(() => { pipe = new DateFormatPipe(); // Pipes можно тестировать без TestBed });
it('should format date correctly', () => { const date = new Date('2024-01-15'); expect(pipe.transform(date, 'short')).toBe('15 янв'); expect(pipe.transform(date, 'full')).toBe('15 января 2024'); });
it('should return empty string for null', () => { expect(pipe.transform(null)).toBe(''); expect(pipe.transform(undefined)).toBe(''); });});Jasmine Matchers
Заголовок раздела «Jasmine Matchers»// Equalityexpect(value).toBe(expected); // ===expect(value).toEqual(expected); // deep equalityexpect(value).not.toBe(other);
// Truthinessexpect(value).toBeTruthy();expect(value).toBeFalsy();expect(value).toBeNull();expect(value).toBeDefined();expect(value).toBeUndefined();
// Numbersexpect(value).toBeGreaterThan(5);expect(value).toBeLessThanOrEqual(10);expect(value).toBeCloseTo(3.14, 2);
// Strings/Arraysexpect(string).toContain('text');expect(array).toContain(item);expect(array).toHaveSize(3);
// Errorsexpect(() => riskyFn()).toThrow();expect(() => riskyFn()).toThrowError('message');
// Spiesexpect(spy).toHaveBeenCalled();expect(spy).toHaveBeenCalledWith(arg1, arg2);expect(spy).toHaveBeenCalledTimes(3);