Kurs Angular · Moduł 11: Testowanie
Testowanie w Angular - Sztuka Wojny
W tej lekcji5
Testowanie to jak przygotowanie do bitwy - sztuka wojny 兵法 (heiho). Samuraj trenuje codziennie, by w prawdziwej walce wykonać perfekcyjne cięcie. Twój kod też musi przejść próbę przed bitwą produkcyjną!
Rodzaje Testów
Jak różne techniki walki, mamy różne poziomy testów:
| Poziom | Cel | Narzędzia |
|---|---|---|
| Unit | Testowanie pojedynczych jednostek | Jasmine, Jest |
| Integration | Testowanie współpracy komponentów | TestBed |
| E2E | Testowanie całej aplikacji | Playwright, Cypress |
Konfiguracja Testów
Angular CLI automatycznie konfiguruje środowisko testowe:
# Uruchomienie testów
ng test
# Testy z coverage
ng test --code-coverage
# Jednorazowe uruchomienie (CI/CD)
ng test --watch=false --browsers=ChromeHeadlessPodstawowa Struktura Testu
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SamuraiComponent } from './samurai.component';
describe('SamuraiComponent', () => {
// Zmienne dostępne dla wszystkich testów
let component: SamuraiComponent;
let fixture: ComponentFixture<SamuraiComponent>;
// Przed każdym testem - przygotowanie
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SamuraiComponent] // Standalone component
}).compileComponents();
fixture = TestBed.createComponent(SamuraiComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // Trigger change detection
});
// Po każdym teście - sprzątanie (opcjonalne)
afterEach(() => {
// Cleanup jeśli potrzebne
});
// Test: czy komponent się tworzy
it('should create', () => {
expect(component).toBeTruthy();
});
// Test: sprawdzanie właściwości
it('should have default level of 1', () => {
expect(component.level).toBe(1);
});
// Test: sprawdzanie szablonu
it('should display samurai name in template', () => {
component.name = 'Musashi';
fixture.detectChanges(); // Aktualizuj widok
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h2')?.textContent).toContain('Musashi');
});
});TestBed - Dojo Testów
TestBed to główne narzędzie do konfiguracji środowiska testowego:
// Minimalna konfiguracja dla standalone component
await TestBed.configureTestingModule({
imports: [SamuraiComponent]
}).compileComponents();
// Z zależnościami
await TestBed.configureTestingModule({
imports: [SamuraiComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
provideRouter([]),
{ provide: SamuraiService, useClass: MockSamuraiService },
{ provide: API_URL, useValue: 'http://test-api' }
]
}).compileComponents();Testowanie @Input i @Output
describe('SamuraiCardComponent', () => {
let component: SamuraiCardComponent;
let fixture: ComponentFixture<SamuraiCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SamuraiCardComponent]
}).compileComponents();
fixture = TestBed.createComponent(SamuraiCardComponent);
component = fixture.componentInstance;
});
it('should display samurai from @Input', () => {
const samurai = { id: 1, name: 'Musashi', clan: 'Ronin' };
component.samurai = samurai; // Ustawiamy @Input
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.name').textContent).toBe('Musashi');
});
it('should emit event on @Output', () => {
const samurai = { id: 1, name: 'Musashi', clan: 'Ronin' };
component.samurai = samurai;
fixture.detectChanges();
// Szpiegujemy emisję eventu
spyOn(component.selected, 'emit'); // Jasmine; w Jest: jest.spyOn(...)
// Symulujemy kliknięcie
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(component.selected.emit).toHaveBeenCalledWith(samurai);
});
});Kod do tej lekcji: src/app/app.component.ts
import { Component, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
// Komponent do testowania
@Component({
selector: 'app-samurai-counter',
standalone: true,
imports: [CommonModule],
template: `
<div class="counter">
<h2>Licznik Samurajów</h2>
<div class="count">{{ count() }}</div>
<div class="controls">
<button (click)="decrement()" [disabled]="count() <= 0">-</button>
<button (click)="increment()">+</button>
<button (click)="reset()">Zeruj</button>
</div>
<p class="status">{{ status() }}</p>
</div>
`,
styles: [`
.counter {
text-align: center;
padding: 20px;
background: rgba(0,0,0,0.3);
border-radius: 10px;
margin: 10px;
}
.count {
font-size: 3em;
font-weight: bold;
color: #e94560;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
margin: 15px 0;
}
button {
padding: 10px 20px;
font-size: 1.2em;
background: #0f3460;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover:not(:disabled) {
background: #e94560;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
color: #888;
}
`]
})
export class SamuraiCounterComponent {
count = signal(0);
status = computed(() => {
const c = this.count();
if (c === 0) return 'Armia jest pusta';
if (c < 5) return 'Mały oddział';
if (c < 10) return 'Średnia armia';
return 'Potężna armia!';
});
increment(): void {
this.count.update(c => c + 1);
}
decrement(): void {
if (this.count() > 0) {
this.count.update(c => c - 1);
}
}
reset(): void {
this.count.set(0);
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, SamuraiCounterComponent],
template: `
<div class="testing-dojo">
<h1>Dojo Testowania - Jasmine & TestBed</h1>
<section class="demo">
<h2>Komponent do testowania:</h2>
<app-samurai-counter></app-samurai-counter>
</section>
<section class="test-code">
<h2>Przykładowy test jednostkowy:</h2>
<pre><code>{{ testCode }}</code></pre>
</section>
<section class="test-run">
<h2>Symulacja testów</h2>
<button (click)="runTests()">Uruchom testy</button>
<div class="test-results">
@for (test of testResults(); track test.name) {
<div class="test-result" [class.passed]="test.passed" [class.failed]="!test.passed">
<span class="icon">{{ test.passed ? '[OK]' : '[BŁĄD]' }}</span>
<span class="name">{{ test.name }}</span>
<span class="time">{{ test.time }}ms</span>
</div>
}
</div>
@if (testResults().length > 0) {
<div class="summary">
Wynik: {{ passedCount() }}/{{ testResults().length }} testów przeszło
</div>
}
</section>
<section class="tips">
<h2>Dobre praktyki testowania</h2>
<ul>
<li><strong>Arrange-Act-Assert:</strong> Przygotuj dane, wykonaj akcję, sprawdź wynik</li>
<li><strong>Jeden test = jeden przypadek:</strong> Każdy test sprawdza jedną funkcjonalność</li>
<li><strong>Niezależność:</strong> Testy nie powinny zależeć od siebie</li>
<li><strong>Nazewnictwo:</strong> Opisowe nazwy testów (should_X_when_Y)</li>
<li><strong>Mockowanie:</strong> Izoluj testowany kod od zależności</li>
</ul>
</section>
</div>
`,
styles: [`
.testing-dojo {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #eee;
}
h1 {
text-align: center;
color: #00d9ff;
}
h2 {
color: #00d9ff;
border-bottom: 2px solid #00d9ff;
padding-bottom: 10px;
}
section {
background: rgba(255,255,255,0.05);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
pre {
background: #0a0a15;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
border-left: 4px solid #00d9ff;
}
code {
color: #e6e6e6;
font-family: 'Fira Code', monospace;
font-size: 0.85em;
}
.test-run button {
padding: 12px 30px;
background: #00d9ff;
color: #1a1a2e;
border: none;
border-radius: 5px;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
margin-bottom: 20px;
}
.test-run button:hover {
background: #00b8d4;
}
.test-results {
margin-top: 15px;
}
.test-result {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
margin: 5px 0;
border-radius: 5px;
background: rgba(0,0,0,0.3);
}
.test-result.passed {
border-left: 4px solid #00e676;
}
.test-result.failed {
border-left: 4px solid #ff5252;
}
.test-result .icon {
font-size: 1.2em;
}
.test-result .name {
flex: 1;
}
.test-result .time {
color: #888;
font-size: 0.9em;
}
.summary {
margin-top: 15px;
padding: 15px;
background: rgba(0,217,255,0.1);
border-radius: 5px;
text-align: center;
font-weight: bold;
}
.tips ul {
list-style: none;
padding: 0;
}
.tips li {
padding: 10px 0;
border-bottom: 1px solid rgba(0,217,255,0.2);
}
.tips li:last-child {
border-bottom: none;
}
`]
})
export class AppComponent {
testResults = signal<TestResult[]>([]);
passedCount = computed(() =>
this.testResults().filter(t => t.passed).length
);
testCode = `describe('SamuraiCounterComponent', () => {
let component: SamuraiCounterComponent;
let fixture: ComponentFixture<SamuraiCounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SamuraiCounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(SamuraiCounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should start with count 0', () => {
expect(component.count()).toBe(0);
});
it('should increment count', () => {
component.increment();
expect(component.count()).toBe(1);
});
it('should decrement count when > 0', () => {
component.count.set(5);
component.decrement();
expect(component.count()).toBe(4);
});
it('should not decrement below 0', () => {
component.count.set(0);
component.decrement();
expect(component.count()).toBe(0);
});
it('should reset to 0', () => {
component.count.set(10);
component.reset();
expect(component.count()).toBe(0);
});
it('should show correct status for empty army', () => {
component.count.set(0);
expect(component.status()).toBe('Armia jest pusta');
});
it('should show correct status for small army', () => {
component.count.set(3);
expect(component.status()).toBe('Mały oddział');
});
});`;
runTests(): void {
this.testResults.set([]);
const tests: TestResult[] = [
{ name: 'should create component', passed: true, time: 12 },
{ name: 'should start with count 0', passed: true, time: 3 },
{ name: 'should increment count', passed: true, time: 5 },
{ name: 'should decrement count when > 0', passed: true, time: 4 },
{ name: 'should not decrement below 0', passed: true, time: 3 },
{ name: 'should reset to 0', passed: true, time: 2 },
{ name: 'should show correct status for empty army', passed: true, time: 4 },
{ name: 'should show correct status for small army', passed: true, time: 3 }
];
// Symulacja uruchamiania testów jeden po drugim
tests.forEach((test, index) => {
setTimeout(() => {
this.testResults.update(results => [...results, test]);
}, (index + 1) * 200);
});
}
}
interface TestResult {
name: string;
passed: boolean;
time: number;
}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co robi metoda fixture.detectChanges() w teście Angular?
2. Który blok Jasmine grupuje powiązane testy w jeden zestaw?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij unit test dla komponentu samuraja sprawdzający wyświetlanie imienia.
- Układanie w pionie
Uporządkuj kroki konfiguracji standalone komponentu w beforeEach (od góry do dołu):
- Klikanie w kolejności
Ułóż linię sprawdzającą, czy komponent został utworzony: expect(component).toBeTruthy()
- Układanie w poziomie
Ułóż wyrażenie pobierające element h1 z wyrenderowanego widoku (jedna linia):