Перейти к содержимому

45. Тестирование: Jasmine/Karma

Тестирование — это страховой полис твоего кода. Angular поставляется с настроенным Jasmine (фреймворк) + Karma (runner) из коробки. Давай разберём всё: от TestBed до async тестов 🛡️


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 (проверка)
});
});

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 newUser = { name: 'Test', email: '[email protected]' };
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);
});
});

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();
});
});

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();
});
}));
});

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);
});

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('');
});
});

// Equality
expect(value).toBe(expected); // ===
expect(value).toEqual(expected); // deep equality
expect(value).not.toBe(other);
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeDefined();
expect(value).toBeUndefined();
// Numbers
expect(value).toBeGreaterThan(5);
expect(value).toBeLessThanOrEqual(10);
expect(value).toBeCloseTo(3.14, 2);
// Strings/Arrays
expect(string).toContain('text');
expect(array).toContain(item);
expect(array).toHaveSize(3);
// Errors
expect(() => riskyFn()).toThrow();
expect(() => riskyFn()).toThrowError('message');
// Spies
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(arg1, arg2);
expect(spy).toHaveBeenCalledTimes(3);