Angular bietet ein umfangreiches Testing-Framework, das eng in die Entwicklungsumgebung integriert ist. Tests in Angular folgen dem Prinzip Prepare, Execute, Assert (Vorbereiten, Ausführen, Überprüfen):
Jede mit Angular CLI erstellte Komponente, Service oder andere Entität wird automatisch mit einer entsprechenden Spec-Datei generiert:
ng generate component my-component
# Erzeugt unter anderem: my-component.component.spec.tsDas Angular TestBed ist die zentrale API für Angular-Tests. Es ermöglicht die Konfiguration einer Testumgebung, die dem realen Angular-Modul ähnelt.
configureTestingModule()createComponent()inject() oder
TestBed.inject()compileComponents()import { TestBed, ComponentFixture } from '@angular/core/testing';
import { MyComponent } from './my-component.component';
import { MyService } from './my-service.service';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let service: MyService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [MyService]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
service = TestBed.inject(MyService);
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
});Jest ist eine beliebte Alternative zum Standard-Testing-Setup in Angular. Es bietet schnellere Testausführung und erweiterte Features.
npm install --save-dev jest @types/jest jest-preset-angularjest.config.js Datei:module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
globalSetup: 'jest-preset-angular/global-setup',
};package.json:"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}Cypress ist ein modernes End-to-End Testing-Framework, das seit Angular 12+ als bevorzugte E2E-Testing-Option dient.
ng add @cypress/schematicdescribe('Login Page', () => {
beforeEach(() => {
cy.visit('/login');
});
it('should display login form', () => {
cy.get('form').should('be.visible');
cy.get('input[name="username"]').should('be.visible');
cy.get('input[name="password"]').should('be.visible');
cy.get('button[type="submit"]').should('be.visible');
});
it('should log in successfully with valid credentials', () => {
cy.get('input[name="username"]').type('testuser');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('.welcome-message').should('contain', 'Welcome, testuser');
});
});Playwright ist ein modernes End-to-End-Testing-Framework von Microsoft, das sich als leistungsstarke Alternative zu Cypress etabliert hat. Es bietet Cross-Browser-Unterstützung, parallele Testausführung und erweiterte Funktionen für moderne Webanwendungen.
# Playwright installieren
npm init playwright@latest
# Oder zu bestehendem Projekt hinzufügen
npm install --save-dev @playwright/test
npx playwright install// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:4200',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}
],
webServer: {
command: 'npm run start',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('should display login form elements', async ({ page }) => {
// Überprüfung der Formularelemente
await expect(page.locator('form')).toBeVisible();
await expect(page.locator('input[name="username"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toBeVisible();
});
test('should show validation errors for empty form', async ({ page }) => {
// Versuche mit leerem Formular einzuloggen
await page.click('button[type="submit"]');
// Überprüfe Validierungsfehler
await expect(page.locator('.error-message')).toContainText('Username is required');
await expect(page.locator('.error-message')).toContainText('Password is required');
});
test('should login successfully with valid credentials', async ({ page }) => {
// Eingabe von gültigen Anmeldedaten
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
// Login-Button klicken
await page.click('button[type="submit"]');
// Überprüfung der erfolgreichen Navigation
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome-message')).toContainText('Welcome, testuser');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.fill('input[name="username"]', 'wronguser');
await page.fill('input[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.locator('.alert-error')).toContainText('Invalid credentials');
await expect(page).toHaveURL('/login');
});
});// e2e/shopping-cart.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Shopping Cart Functionality', () => {
test.beforeEach(async ({ page }) => {
// Login vor jedem Test
await page.goto('/login');
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('should add product to cart', async ({ page }) => {
await page.goto('/products');
// Warten auf das Laden der Produkte
await page.waitForSelector('.product-item');
// Erstes Produkt auswählen
const firstProduct = page.locator('.product-item').first();
const productName = await firstProduct.locator('.product-name').textContent();
const productPrice = await firstProduct.locator('.product-price').textContent();
// Zum Warenkorb hinzufügen
await firstProduct.locator('.add-to-cart-btn').click();
// Bestätigung überprüfen
await expect(page.locator('.cart-notification')).toContainText('Product added to cart');
// Warenkorb öffnen
await page.click('.cart-icon');
// Überprüfung der Warenkorb-Inhalte
const cartItem = page.locator('.cart-item').first();
await expect(cartItem.locator('.item-name')).toContainText(productName || '');
await expect(cartItem.locator('.item-price')).toContainText(productPrice || '');
});
test('should update product quantity in cart', async ({ page }) => {
// Produkt zum Warenkorb hinzufügen (Setup)
await page.goto('/products');
await page.waitForSelector('.product-item');
await page.locator('.product-item').first().locator('.add-to-cart-btn').click();
// Warenkorb öffnen
await page.click('.cart-icon');
// Menge erhöhen
const quantityInput = page.locator('.quantity-input').first();
await quantityInput.fill('3');
await quantityInput.press('Enter');
// Warten auf Update
await page.waitForTimeout(500);
// Überprüfung der aktualisierten Gesamtsumme
const totalPrice = page.locator('.total-price');
await expect(totalPrice).not.toBeEmpty();
});
test('should complete checkout process', async ({ page }) => {
// Produkt hinzufügen und Checkout starten
await page.goto('/products');
await page.waitForSelector('.product-item');
await page.locator('.product-item').first().locator('.add-to-cart-btn').click();
await page.click('.cart-icon');
await page.click('.checkout-btn');
// Checkout-Formular ausfüllen
await page.fill('input[name="firstName"]', 'John');
await page.fill('input[name="lastName"]', 'Doe');
await page.fill('input[name="email"]', 'john.doe@example.com');
await page.fill('input[name="address"]', '123 Main St');
await page.fill('input[name="city"]', 'Anytown');
await page.fill('input[name="zipCode"]', '12345');
// Zahlungsinformationen
await page.fill('input[name="cardNumber"]', '4111111111111111');
await page.fill('input[name="expiryDate"]', '12/25');
await page.fill('input[name="cvv"]', '123');
// Bestellung abschließen
await page.click('.place-order-btn');
// Bestätigung überprüfen
await expect(page).toHaveURL('/order-confirmation');
await expect(page.locator('.success-message')).toContainText('Order placed successfully');
});
});// e2e/pages/login.page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('input[name="username"]');
this.passwordInput = page.locator('input[name="password"]');
this.loginButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.alert-error');
}
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectErrorMessage(message: string) {
await this.errorMessage.waitFor();
const text = await this.errorMessage.textContent();
return text?.includes(message) || false;
}
}
// e2e/pages/dashboard.page.ts
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly userMenu: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.locator('.welcome-message');
this.userMenu = page.locator('.user-menu');
this.logoutButton = page.locator('.logout-btn');
}
async expectWelcomeMessage(username: string) {
await this.welcomeMessage.waitFor();
const text = await this.welcomeMessage.textContent();
return text?.includes(`Welcome, ${username}`) || false;
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
}
}
// Verwendung des Page Object Models:
// e2e/login-with-pom.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';
import { DashboardPage } from './pages/dashboard.page';
test('should login using Page Object Model', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await expect(page).toHaveURL('/dashboard');
expect(await dashboardPage.expectWelcomeMessage('testuser')).toBeTruthy();
});// e2e/api/user-api.spec.ts
import { test, expect } from '@playwright/test';
test.describe('User API Tests', () => {
let authToken: string;
test.beforeAll(async ({ request }) => {
// Authentifizierung für API-Tests
const response = await request.post('/api/auth/login', {
data: {
username: 'testuser',
password: 'password123'
}
});
expect(response.ok()).toBeTruthy();
const responseBody = await response.json();
authToken = responseBody.token;
});
test('should get user profile', async ({ request }) => {
const response = await request.get('/api/user/profile', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('username', 'testuser');
});
test('should update user profile', async ({ request }) => {
const updatedUser = {
firstName: 'Updated',
lastName: 'User',
email: 'updated@example.com'
};
const response = await request.put('/api/user/profile', {
headers: {
'Authorization': `Bearer ${authToken}`
},
data: updatedUser
});
expect(response.ok()).toBeTruthy();
const responseBody = await response.json();
expect(responseBody.firstName).toBe('Updated');
expect(responseBody.lastName).toBe('User');
});
});// e2e/mobile.spec.ts
import { test, expect, devices } from '@playwright/test';
test.describe('Mobile Responsive Tests', () => {
test('should display mobile navigation', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 12']
});
const page = await context.newPage();
await page.goto('/');
// Mobile Menu sollte sichtbar sein
await expect(page.locator('.mobile-menu-toggle')).toBeVisible();
// Desktop Menu sollte versteckt sein
await expect(page.locator('.desktop-nav')).toBeHidden();
// Mobile Menu öffnen
await page.click('.mobile-menu-toggle');
await expect(page.locator('.mobile-nav')).toBeVisible();
await context.close();
});
test('should handle touch gestures', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPad Pro']
});
const page = await context.newPage();
await page.goto('/products');
// Touch-Scroll simulieren
await page.touchscreen.tap(200, 300);
await page.mouse.wheel(0, -500);
// Überprüfung, dass Scroll funktioniert hat
const scrollPosition = await page.evaluate(() => window.pageYOffset);
expect(scrollPosition).toBeGreaterThan(0);
await context.close();
});
});// e2e/visual-regression.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Visual Regression Tests', () => {
test('should match homepage screenshot', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Screenshot der gesamten Seite
await expect(page).toHaveScreenshot('homepage.png');
});
test('should match component screenshot', async ({ page }) => {
await page.goto('/products');
// Screenshot eines spezifischen Elements
const productCard = page.locator('.product-card').first();
await expect(productCard).toHaveScreenshot('product-card.png');
});
test('should match mobile layout', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
});// e2e/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
type AuthFixtures = {
authenticatedPage: any;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
// Automatische Anmeldung für jeden Test
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await page.waitForURL('/dashboard');
await use(page);
},
});
// Verwendung der Fixture:
// e2e/authenticated-tests.spec.ts
import { test, expect } from './fixtures/auth.fixture';
test('should access protected route', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/admin');
await expect(authenticatedPage.locator('.admin-panel')).toBeVisible();
});// e2e/performance.spec.ts
import { test, expect } from '@playwright/test';
test('should meet performance benchmarks', async ({ page }) => {
// Performance-Metriken erfassen
await page.goto('/', { waitUntil: 'networkidle' });
const performanceMetrics = await page.evaluate(() => {
return JSON.stringify(performance.timing);
});
const metrics = JSON.parse(performanceMetrics);
const loadTime = metrics.loadEventEnd - metrics.navigationStart;
// Überprüfung, dass die Seite in unter 3 Sekunden lädt
expect(loadTime).toBeLessThan(3000);
});
test('should handle large datasets efficiently', async ({ page }) => {
await page.goto('/users');
// Messen der Zeit für das Laden einer großen Benutzerliste
const startTime = Date.now();
await page.waitForSelector('.user-item:nth-child(100)', { timeout: 10000 });
const endTime = Date.now();
const loadTime = endTime - startTime;
expect(loadTime).toBeLessThan(5000);
});// package.json Scripts
{
"scripts": {
"e2e": "playwright test",
"e2e:headed": "playwright test --headed",
"e2e:debug": "playwright test --debug",
"e2e:report": "playwright show-report",
"e2e:ui": "playwright test --ui"
}
}# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30Verwenden Sie data-testid Attribute: Stabile Selektoren für Tests
<button data-testid="submit-button">Submit</button>Implementieren Sie Page Object Model: Bessere Wartbarkeit und Wiederverwendbarkeit
Nutzen Sie Auto-Warten: Playwright wartet automatisch auf Elemente - vermeiden Sie explizite Waits
Parallele Testausführung: Konfigurieren Sie Worker für schnellere Testausführung
Cross-Browser-Testing: Testen Sie in allen relevanten Browsern
Visual Regression Testing: Nutzen Sie Screenshots für UI-Konsistenz
Unit-Tests prüfen einzelne Funktionen, Klassen oder Komponenten isoliert von anderen Teilen der Anwendung.
import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [CalculatorService]
});
service = TestBed.inject(CalculatorService);
});
it('should add two numbers correctly', () => {
expect(service.add(2, 3)).toBe(5);
});
it('should divide two numbers correctly', () => {
expect(service.divide(10, 2)).toBe(5);
});
it('should throw error when dividing by zero', () => {
expect(() => service.divide(10, 0)).toThrowError('Cannot divide by zero');
});
});Integrationstests prüfen, wie verschiedene Komponenten, Services oder Module zusammenarbeiten.
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';
import { By } from '@angular/platform-browser';
describe('Parent-Child Integration', () => {
let parentFixture: ComponentFixture<ParentComponent>;
let parentComponent: ParentComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent]
}).compileComponents();
parentFixture = TestBed.createComponent(ParentComponent);
parentComponent = parentFixture.componentInstance;
parentFixture.detectChanges();
});
it('should pass data from parent to child', () => {
parentComponent.dataForChild = 'Test Data';
parentFixture.detectChanges();
const childComponent = parentFixture.debugElement
.query(By.directive(ChildComponent))
.componentInstance;
expect(childComponent.data).toBe('Test Data');
});
it('should handle event from child to parent', () => {
const childDebugElement = parentFixture.debugElement.query(By.directive(ChildComponent));
const childComponent = childDebugElement.componentInstance;
spyOn(parentComponent, 'handleChildEvent');
childComponent.buttonClicked.emit('Child Event');
expect(parentComponent.handleChildEvent).toHaveBeenCalledWith('Child Event');
});
});End-to-End-Tests (E2E) simulieren die Benutzerinteraktion mit der Anwendung und testen den kompletten Anwendungsfluss.
describe('Todo App', () => {
beforeEach(() => {
cy.visit('/');
});
it('should add a new todo item', () => {
cy.get('.todo-input').type('Learn Cypress Testing');
cy.get('.add-button').click();
cy.get('.todo-list').should('contain', 'Learn Cypress Testing');
cy.get('.todo-count').should('contain', '1 item left');
});
it('should mark a todo as completed', () => {
// Add a todo first
cy.get('.todo-input').type('Learn Cypress Testing');
cy.get('.add-button').click();
// Mark it as completed
cy.get('.todo-item').first().find('.toggle').click();
// Verify it's marked as completed
cy.get('.todo-item').first().should('have.class', 'completed');
cy.get('.todo-count').should('contain', '0 items left');
});
});import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ParentComponent } from './parent.component';
describe('ParentComponent (Shallow)', () => {
let component: ParentComponent;
let fixture: ComponentFixture<ParentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ParentComponent],
schemas: [NO_ERRORS_SCHEMA] // Ignoriert unbekannte Elemente und Attribute
}).compileComponents();
fixture = TestBed.createComponent(ParentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';
import { GrandchildComponent } from './grandchild.component';
describe('ParentComponent (Deep)', () => {
let component: ParentComponent;
let fixture: ComponentFixture<ParentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ParentComponent,
ChildComponent,
GrandchildComponent
]
}).compileComponents();
fixture = TestBed.createComponent(ParentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create with all child components', () => {
const childElements = fixture.debugElement.queryAll(By.directive(ChildComponent));
expect(childElements.length).toBe(2); // Angenommen, es gibt 2 Kind-Komponenten
});
});Angular-Komponenten haben verschiedene Arten von Bindings, die getestet werden sollten:
it('should bind header text correctly', () => {
component.headerText = 'Test Header';
fixture.detectChanges();
const headerElement = fixture.debugElement.query(By.css('h1')).nativeElement;
expect(headerElement.textContent).toBe('Test Header');
});it('should call onSave method when save button is clicked', () => {
spyOn(component, 'onSave');
const saveButton = fixture.debugElement.query(By.css('.save-button')).nativeElement;
saveButton.click();
expect(component.onSave).toHaveBeenCalled();
});it('should update model when input value changes', () => {
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
inputElement.value = 'New Value';
inputElement.dispatchEvent(new Event('input'));
expect(component.inputValue).toBe('New Value');
});Ereignisse in Angular-Komponenten umfassen DOM-Ereignisse und benutzerdefinierte Angular-Ereignisse:
it('should update counter on button click', () => {
expect(component.counter).toBe(0);
const button = fixture.debugElement.query(By.css('.increment-button')).nativeElement;
button.click();
expect(component.counter).toBe(1);
});it('should emit selected item when item is clicked', () => {
let selectedItem: any;
component.itemSelected.subscribe((item: any) => selectedItem = item);
component.selectItem({ id: 1, name: 'Test Item' });
expect(selectedItem).toEqual({ id: 1, name: 'Test Item' });
});Angular-Komponenten haben verschiedene Lifecycle Hooks, die wichtige Logik enthalten können:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DataComponent } from './data.component';
import { DataService } from './data.service';
import { of } from 'rxjs';
describe('DataComponent', () => {
let component: DataComponent;
let fixture: ComponentFixture<DataComponent>;
let dataService: jasmine.SpyObj<DataService>;
beforeEach(async () => {
const dataServiceSpy = jasmine.createSpyObj('DataService', ['getData']);
await TestBed.configureTestingModule({
declarations: [DataComponent],
providers: [
{ provide: DataService, useValue: dataServiceSpy }
]
}).compileComponents();
fixture = TestBed.createComponent(DataComponent);
component = fixture.componentInstance;
dataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
});
it('should load data on ngOnInit', () => {
const testData = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
dataService.getData.and.returnValue(of(testData));
// ngOnInit wird automatisch aufgerufen, wenn detectChanges aufgerufen wird
fixture.detectChanges();
expect(dataService.getData).toHaveBeenCalled();
expect(component.items).toEqual(testData);
});
});import { SimpleChange } from '@angular/core';
it('should update computed values when inputs change', () => {
// Set initial values
component.firstName = 'John';
component.lastName = 'Doe';
// Simulate ngOnChanges with SimpleChanges object
component.ngOnChanges({
firstName: new SimpleChange(null, 'John', true),
lastName: new SimpleChange(null, 'Doe', true)
});
expect(component.fullName).toBe('John Doe');
// Test subsequent change
component.ngOnChanges({
firstName: new SimpleChange('John', 'Jane', false)
});
expect(component.fullName).toBe('Jane Doe');
});Angular Services können Abhängigkeiten haben, die beim Testen gemockt werden müssen:
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { AuthService } from './auth.service';
import { LoggerService } from './logger.service';
describe('UserService', () => {
let userService: UserService;
let authServiceSpy: jasmine.SpyObj<AuthService>;
let loggerServiceSpy: jasmine.SpyObj<LoggerService>;
beforeEach(() => {
const authSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated', 'getCurrentUser']);
const loggerSpy = jasmine.createSpyObj('LoggerService', ['log', 'error']);
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: AuthService, useValue: authSpy },
{ provide: LoggerService, useValue: loggerSpy }
]
});
userService = TestBed.inject(UserService);
authServiceSpy = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
loggerServiceSpy = TestBed.inject(LoggerService) as jasmine.SpyObj<LoggerService>;
});
it('should return user profile when authenticated', () => {
const mockUser = { id: 1, name: 'Test User' };
authServiceSpy.isAuthenticated.and.returnValue(true);
authServiceSpy.getCurrentUser.and.returnValue(mockUser);
const result = userService.getUserProfile();
expect(authServiceSpy.isAuthenticated).toHaveBeenCalled();
expect(authServiceSpy.getCurrentUser).toHaveBeenCalled();
expect(result).toEqual(mockUser);
});
it('should return null when not authenticated', () => {
authServiceSpy.isAuthenticated.and.returnValue(false);
const result = userService.getUserProfile();
expect(authServiceSpy.isAuthenticated).toHaveBeenCalled();
expect(authServiceSpy.getCurrentUser).not.toHaveBeenCalled();
expect(result).toBeNull();
});
});Für das Testen von Services, die HTTP-Anfragen stellen, bietet
Angular das HttpClientTestingModule:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ProductService } from './product.service';
import { Product } from './product.model';
describe('ProductService', () => {
let service: ProductService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ProductService]
});
service = TestBed.inject(ProductService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Stellt sicher, dass keine unbeantworteten Anfragen übrig sind
});
it('should get all products via GET', () => {
const mockProducts: Product[] = [
{ id: 1, name: 'Product 1', price: 100 },
{ id: 2, name: 'Product 2', price: 200 }
];
service.getProducts().subscribe(products => {
expect(products).toEqual(mockProducts);
});
const req = httpMock.expectOne('api/products');
expect(req.request.method).toBe('GET');
req.flush(mockProducts);
});
it('should create a product via POST', () => {
const newProduct: Product = { id: 3, name: 'Product 3', price: 300 };
service.createProduct(newProduct).subscribe(product => {
expect(product).toEqual(newProduct);
});
const req = httpMock.expectOne('api/products');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(newProduct);
req.flush(newProduct);
});
it('should handle error correctly', () => {
service.getProducts().subscribe({
next: () => fail('should have failed with a 404'),
error: (error) => {
expect(error.status).toBe(404);
expect(error.statusText).toBe('Not Found');
}
});
const req = httpMock.expectOne('api/products');
req.flush('Not found', { status: 404, statusText: 'Not Found' });
});
});Strukturelle und Attribut-Direktiven können mit Hilfe einer Test-Komponente getestet werden:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
// Testkomponente, die die zu testende Direktive verwendet
@Component({
template: `
<div [appHighlight]="color" defaultColor="yellow">Highlight me!</div>
`
})
class TestComponent {
color = 'red';
}
describe('HighlightDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let highlightedElement: DebugElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HighlightDirective, TestComponent]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
highlightedElement = fixture.debugElement.query(By.css('div'));
fixture.detectChanges();
});
it('should highlight with default color when no input is provided', () => {
component.color = '';
fixture.detectChanges();
expect(highlightedElement.nativeElement.style.backgroundColor).toBe('yellow');
});
it('should highlight with provided color', () => {
expect(highlightedElement.nativeElement.style.backgroundColor).toBe('red');
});
it('should change highlight color when input changes', () => {
component.color = 'blue';
fixture.detectChanges();
expect(highlightedElement.nativeElement.style.backgroundColor).toBe('blue');
});
});Pipes können direkt als Klassen getestet werden:
import { DateFormatPipe } from './date-format.pipe';
describe('DateFormatPipe', () => {
let pipe: DateFormatPipe;
beforeEach(() => {
pipe = new DateFormatPipe();
});
it('should format date correctly with default format', () => {
const date = new Date(2023, 0, 15); // 15. Januar 2023
const result = pipe.transform(date);
expect(result).toBe('15.01.2023');
});
it('should format date with custom format', () => {
const date = new Date(2023, 0, 15);
const result = pipe.transform(date, 'yyyy/MM/dd');
expect(result).toBe('2023/01/15');
});
it('should return empty string for null input', () => {
const result = pipe.transform(null);
expect(result).toBe('');
});
});Mocking und Spying sind wichtige Techniken, um isolierte Tests zu schreiben.
Spies können verwendet werden, um Methodenaufrufe zu überwachen und Rückgabewerte zu kontrollieren:
it('should call service method when button is clicked', () => {
// Spy auf eine Methode setzen
spyOn(dataService, 'getData').and.returnValue(of(['item1', 'item2']));
// Aktion auslösen
const button = fixture.debugElement.query(By.css('.load-button'));
button.nativeElement.click();
// Überprüfen
expect(dataService.getData).toHaveBeenCalled();
expect(component.items.length).toBe(2);
});// Spy-Objekt mit mehreren Methoden erstellen
const serviceStub = jasmine.createSpyObj('DataService', ['getData', 'updateData', 'deleteData']);
serviceStub.getData.and.returnValue(of(['item1', 'item2']));
serviceStub.updateData.and.returnValue(of(true));
// In TestBed bereitstellen
TestBed.configureTestingModule({
providers: [
{ provide: DataService, useValue: serviceStub }
]
});Manchmal ist es einfacher, einen vollständigen Mock-Service zu erstellen:
// Mock-Service-Klasse
class MockAuthService {
isAuthenticated = true;
login(username: string, password: string) {
return of({ success: true, user: { id: 1, name: username } });
}
logout() {
this.isAuthenticated = false;
return of(true);
}
}
// In TestBed bereitstellen
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useClass: MockAuthService }
]
});Das Testen von reaktiver Programmierung mit RxJS erfordert spezielle Techniken.
import { fakeAsync, tick } from '@angular/core/testing';
import { of, throwError, timer } from 'rxjs';
import { delay, map, catchError } from 'rxjs/operators';
it('should handle delayed observable', fakeAsync(() => {
let result: any;
// Observable mit Verzögerung
of('delayed value').pipe(
delay(1000)
).subscribe(value => {
result = value;
});
// Anfangs kein Ergebnis
expect(result).toBeUndefined();
// Zeit voranschreiten lassen
tick(1000);
// Nach Verzögerung sollte Wert gesetzt sein
expect(result).toBe('delayed value');
}));Für komplexere RxJS-Tests kann der TestScheduler
verwendet werden:
import { TestScheduler } from 'rxjs/testing';
describe('RxJS Testing', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should debounce input values', () => {
testScheduler.run(({ hot, expectObservable }) => {
const source = hot('-a-b-c-d-|', { a: 1, b: 2, c: 3, d: 4 });
const result = source.pipe(debounceTime(2));
const expected = '-----b-c-d|';
expectObservable(result).toBe(expected, { b: 2, c: 3, d: 4 });
});
});
});Template-Driven Forms sind eine einfache Möglichkeit, Formulare in Angular zu erstellen. Ihre Tests konzentrieren sich auf DOM-Interaktionen und NgModel-Bindings.
import { Component } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
@Component({
template: `
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
<input name="username" [(ngModel)]="user.username" required>
<input name="email" [(ngModel)]="user.email" required email>
<button type="submit" [disabled]="!myForm.valid">Submit</button>
</form>
`
})
class TestFormComponent {
user = { username: '', email: '' };
submitted = false;
onSubmit(formValue: any) {
this.submitted = true;
this.user = formValue;
}
}
describe('Template-Driven Form', () => {
let component: TestFormComponent;
let fixture: ComponentFixture<TestFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [TestFormComponent]
}).compileComponents();
fixture = TestBed.createComponent(TestFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the form with empty values', () => {
const usernameInput = fixture.debugElement.query(By.css('input[name="username"]')).nativeElement;
const emailInput = fixture.debugElement.query(By.css('input[name="email"]')).nativeElement;
expect(usernameInput.value).toBe('');
expect(emailInput.value).toBe('');
});
it('should disable submit button when form is invalid', () => {
const submitButton = fixture.debugElement.query(By.css('button[type="submit"]')).nativeElement;
expect(submitButton.disabled).toBeTruthy();
});
it('should enable submit button when form is valid', fakeAsync(() => {
const usernameInput = fixture.debugElement.query(By.css('input[name="username"]')).nativeElement;
const emailInput = fixture.debugElement.query(By.css('input[name="email"]')).nativeElement;
const submitButton = fixture.debugElement.query(By.css('button[type="submit"]')).nativeElement;
usernameInput.value = 'testuser';
usernameInput.dispatchEvent(new Event('input'));
emailInput.value = 'test@example.com';
emailInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
tick(); // Warte auf asynchrone Validierung
fixture.detectChanges();
expect(submitButton.disabled).toBeFalsy();
}));
it('should call onSubmit method when form is submitted', fakeAsync(() => {
spyOn(component, 'onSubmit').and.callThrough();
// Formular ausfüllen
const usernameInput = fixture.debugElement.query(By.css('input[name="username"]')).nativeElement;
const emailInput = fixture.debugElement.query(By.css('input[name="email"]')).nativeElement;
const form = fixture.debugElement.query(By.css('form')).nativeElement;
usernameInput.value = 'testuser';
usernameInput.dispatchEvent(new Event('input'));
emailInput.value = 'test@example.com';
emailInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
tick();
fixture.detectChanges();
// Formular absenden
form.dispatchEvent(new Event('submit'));
expect(component.onSubmit).toHaveBeenCalled();
expect(component.submitted).toBeTruthy();
expect(component.user.username).toBe('testuser');
expect(component.user.email).toBe('test@example.com');
}));
});Reactive Forms bieten mehr Kontrolle und sind daher oft einfacher zu testen als Template-Driven Forms.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
@Component({
template: `
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<input formControlName="username">
<div *ngIf="username.invalid && username.touched" class="error">
Username is required
</div>
<input formControlName="email">
<div *ngIf="email.invalid && email.touched" class="error">
Valid email is required
</div>
<input type="password" formControlName="password">
<div *ngIf="password.invalid && password.touched" class="error">
Password must be at least 6 characters
</div>
<button type="submit" [disabled]="registerForm.invalid">Register</button>
</form>
`
})
class ReactiveFormComponent implements OnInit {
registerForm!: FormGroup;
submitted = false;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.registerForm = this.fb.group({
username: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
get username() { return this.registerForm.get('username')!; }
get email() { return this.registerForm.get('email')!; }
get password() { return this.registerForm.get('password')!; }
onSubmit() {
this.submitted = true;
if (this.registerForm.valid) {
// Logik für erfolgreiche Registrierung
}
}
}
describe('Reactive Form Component', () => {
let component: ReactiveFormComponent;
let fixture: ComponentFixture<ReactiveFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [ReactiveFormComponent]
}).compileComponents();
fixture = TestBed.createComponent(ReactiveFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create form with 3 controls', () => {
expect(component.registerForm.contains('username')).toBeTruthy();
expect(component.registerForm.contains('email')).toBeTruthy();
expect(component.registerForm.contains('password')).toBeTruthy();
});
it('should mark username as invalid when empty', () => {
const control = component.registerForm.get('username');
control?.setValue('');
expect(control?.valid).toBeFalsy();
});
it('should mark username as valid when it has a value', () => {
const control = component.registerForm.get('username');
control?.setValue('testuser');
expect(control?.valid).toBeTruthy();
});
it('should mark email as invalid when not in correct format', () => {
const control = component.registerForm.get('email');
control?.setValue('invalid-email');
expect(control?.valid).toBeFalsy();
});
it('should mark password as invalid when less than 6 characters', () => {
const control = component.registerForm.get('password');
control?.setValue('12345');
expect(control?.valid).toBeFalsy();
});
it('should disable submit button when form is invalid', () => {
// Form ist initial leer und somit invalid
const submitButton = fixture.debugElement.query(By.css('button[type="submit"]')).nativeElement;
expect(submitButton.disabled).toBeTruthy();
});
it('should enable submit button when form is valid', () => {
// Formular mit gültigen Werten ausfüllen
component.registerForm.setValue({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
fixture.detectChanges();
const submitButton = fixture.debugElement.query(By.css('button[type="submit"]')).nativeElement;
expect(submitButton.disabled).toBeFalsy();
});
it('should call onSubmit method when form is submitted', () => {
spyOn(component, 'onSubmit');
// Formular mit gültigen Werten ausfüllen
component.registerForm.setValue({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
fixture.detectChanges();
// Formular absenden
const form = fixture.debugElement.query(By.css('form'));
form.triggerEventHandler('submit', null);
expect(component.onSubmit).toHaveBeenCalled();
});
it('should show error message when username field is touched and empty', () => {
// Username-Feld berühren und leer lassen
const usernameControl = component.registerForm.get('username');
usernameControl?.setValue('');
usernameControl?.markAsTouched();
fixture.detectChanges();
const errorMessage = fixture.debugElement.query(By.css('.error'));
expect(errorMessage).toBeTruthy();
expect(errorMessage.nativeElement.textContent).toContain('Username is required');
});
});Beim Testen der Formularvalidierung ist es wichtig, sowohl die Validierungslogik als auch die Anzeige von Fehlermeldungen zu überprüfen.
it('should validate email format correctly', () => {
const emailControl = component.registerForm.get('email');
// Ungültige Email
emailControl?.setValue('not-an-email');
expect(emailControl?.hasError('email')).toBeTruthy();
// Gültige Email
emailControl?.setValue('valid@example.com');
expect(emailControl?.hasError('email')).toBeFalsy();
});
it('should validate password complexity', () => {
const passwordControl = component.registerForm.get('password');
const complexPasswordValidator = (control: AbstractControl) => {
const value = control.value;
const hasNumber = /\d/.test(value);
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
return hasNumber && hasUpper && hasLower ? null : { complexity: true };
};
// Validator zur Laufzeit hinzufügen
passwordControl?.setValidators([
Validators.required,
Validators.minLength(6),
complexPasswordValidator
]);
passwordControl?.updateValueAndValidity();
// Einfaches Passwort
passwordControl?.setValue('simple');
expect(passwordControl?.hasError('complexity')).toBeTruthy();
// Komplexes Passwort
passwordControl?.setValue('Complex123');
expect(passwordControl?.hasError('complexity')).toBeFalsy();
});Asynchrone Validatoren erfordern besondere Aufmerksamkeit beim Testen:
import { fakeAsync, tick } from '@angular/core/testing';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
// Mock-Service für asynchrone Validierung
class UserService {
isUsernameTaken(username: string) {
const isTaken = username === 'existinguser';
return of(isTaken).pipe(delay(100));
}
}
// Asynchroner Validator
const usernameAsyncValidator = (userService: UserService) => {
return (control: AbstractControl) => {
return userService.isUsernameTaken(control.value).pipe(
map(isTaken => isTaken ? { usernameTaken: true } : null)
);
};
};
describe('Async Validator Tests', () => {
let component: ReactiveFormComponent;
let fixture: ComponentFixture<ReactiveFormComponent>;
let userService: UserService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [ReactiveFormComponent],
providers: [UserService]
}).compileComponents();
fixture = TestBed.createComponent(ReactiveFormComponent);
component = fixture.componentInstance;
userService = TestBed.inject(UserService);
// FormGroup mit asynchronem Validator erstellen
component.registerForm = new FormBuilder().group({
username: ['', Validators.required, usernameAsyncValidator(userService)]
});
fixture.detectChanges();
});
it('should validate username availability asynchronously', fakeAsync(() => {
const usernameControl = component.registerForm.get('username');
// Prüfen eines bereits vorhandenen Benutzernamens
usernameControl?.setValue('existinguser');
tick(100); // Warte auf die asynchrone Validierung
expect(usernameControl?.hasError('usernameTaken')).toBeTruthy();
expect(usernameControl?.valid).toBeFalsy();
// Prüfen eines verfügbaren Benutzernamens
usernameControl?.setValue('newuser');
tick(100);
expect(usernameControl?.hasError('usernameTaken')).toBeFalsy();
expect(usernameControl?.valid).toBeTruthy();
}));
});Das Testen von Routing in Angular ist ein wichtiger Aspekt, um die Navigation und den Zustand der Anwendung zu überprüfen.
Zum Testen des Routings benötigen wir das
RouterTestingModule:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { routes } from './app-routing.module';
// Stub-Komponenten für Routes
@Component({ template: '<div>Home Component</div>' })
class HomeComponent {}
@Component({ template: '<div>About Component</div>' })
class AboutComponent {}
@Component({ template: '<div>Contact Component</div>' })
class ContactComponent {}
@Component({ template: '<router-outlet></router-outlet>' })
class AppComponent {}
describe('Router Navigation', () => {
let router: Router;
let location: Location;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async () => {
const testRoutes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent }
];
await TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(testRoutes)],
declarations: [
AppComponent,
HomeComponent,
AboutComponent,
ContactComponent
]
}).compileComponents();
router = TestBed.inject(Router);
location = TestBed.inject(Location);
fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
});
it('should navigate to "" redirects to /home', async () => {
await router.navigateByUrl('');
expect(location.path()).toBe('/');
});
it('should navigate to "about"', async () => {
await router.navigateByUrl('/about');
expect(location.path()).toBe('/about');
});
it('should navigate to "contact"', async () => {
await router.navigateByUrl('/contact');
expect(location.path()).toBe('/contact');
});
});Um zu testen, ob Links richtig funktionieren:
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { By } from '@angular/platform-browser';
import { RouterLinkWithHref } from '@angular/router';
@Component({
template: `
<nav>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
<a routerLink="/contact">Contact</a>
</nav>
`
})
class NavComponent {}
describe('NavComponent RouterLinks', () => {
let component: NavComponent;
let fixture: ComponentFixture<NavComponent>;
let router: Router;
let debugElements: any[];
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [NavComponent]
}).compileComponents();
fixture = TestBed.createComponent(NavComponent);
component = fixture.componentInstance;
router = TestBed.inject(Router);
fixture.detectChanges();
// Sammle alle RouterLink-Elemente
debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
});
it('should have 3 RouterLinks', () => {
expect(debugElements.length).toBe(3);
});
it('should have a RouterLink to "/"', () => {
const homeLink = debugElements.find(de => de.attributes['routerLink'] === '/');
expect(homeLink).toBeTruthy();
expect(homeLink.nativeElement.textContent).toBe('Home');
});
it('should have a RouterLink to "/about"', () => {
const aboutLink = debugElements.find(de => de.attributes['routerLink'] === '/about');
expect(aboutLink).toBeTruthy();
expect(aboutLink.nativeElement.textContent).toBe('About');
});
it('should navigate when link is clicked', () => {
spyOn(router, 'navigateByUrl');
const aboutLink = debugElements.find(de => de.attributes['routerLink'] === '/about');
aboutLink.triggerEventHandler('click', { button: 0 });
const routerCall = (router.navigateByUrl as jasmine.Spy).calls.first().args[0];
expect(routerCall.toString()).toBe('/about');
});
});Route-Guards können wie folgt getestet werden:
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
describe('AuthGuard', () => {
let guard: AuthGuard;
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
let routeSnapshot: ActivatedRouteSnapshot;
let routerStateSnapshot: RouterStateSnapshot;
beforeEach(() => {
const authServiceSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']);
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [
AuthGuard,
{ provide: AuthService, useValue: authServiceSpy },
{ provide: Router, useValue: routerSpy }
]
});
guard = TestBed.inject(AuthGuard);
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
routeSnapshot = new ActivatedRouteSnapshot();
routerStateSnapshot = { url: '/admin' } as RouterStateSnapshot;
});
it('should allow access to authenticated users', () => {
authService.isAuthenticated.and.returnValue(true);
const result = guard.canActivate(routeSnapshot, routerStateSnapshot);
expect(result).toBe(true);
expect(router.navigate).not.toHaveBeenCalled();
});
it('should redirect unauthenticated users to login page', () => {
authService.isAuthenticated.and.returnValue(false);
const result = guard.canActivate(routeSnapshot, routerStateSnapshot);
expect(result).toBe(false);
expect(router.navigate).toHaveBeenCalledWith(['/login'], { queryParams: { returnUrl: '/admin' } });
});
});Um Routen mit Parametern zu testen:
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
@Component({
template: `<div>User ID: {{userId}}</div>`
})
class UserDetailComponent {
userId: string = '';
constructor(private route: ActivatedRoute) {
this.route.paramMap.subscribe(params => {
this.userId = params.get('id') || '';
});
}
}
describe('UserDetailComponent', () => {
let component: UserDetailComponent;
let fixture: ComponentFixture<UserDetailComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [UserDetailComponent],
providers: [
{
provide: ActivatedRoute,
useValue: {
paramMap: of(convertToParamMap({ id: '123' }))
}
}
]
}).compileComponents();
fixture = TestBed.createComponent(UserDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display the user ID from route parameter', () => {
expect(component.userId).toBe('123');
expect(fixture.nativeElement.textContent).toContain('User ID: 123');
});
});Router-Resolver können wie folgt getestet werden:
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { UserDataResolver } from './user-data.resolver';
import { UserService } from './user.service';
import { of } from 'rxjs';
describe('UserDataResolver', () => {
let resolver: UserDataResolver;
let userService: jasmine.SpyObj<UserService>;
let routeSnapshot: ActivatedRouteSnapshot;
let routerStateSnapshot: RouterStateSnapshot;
beforeEach(() => {
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUserById']);
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [
UserDataResolver,
{ provide: UserService, useValue: userServiceSpy }
]
});
resolver = TestBed.inject(UserDataResolver);
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
routeSnapshot = new ActivatedRouteSnapshot();
routeSnapshot.params = { id: '123' };
routerStateSnapshot = { url: '/users/123' } as RouterStateSnapshot;
});
it('should resolve user data from service', () => {
const mockUser = { id: '123', name: 'Test User' };
userService.getUserById.and.returnValue(of(mockUser));
resolver.resolve(routeSnapshot, routerStateSnapshot).subscribe(result => {
expect(result).toEqual(mockUser);
});
expect(userService.getUserById).toHaveBeenCalledWith('123');
});
});Ein praktisches Beispiel mit Formulareinreichung und Navigation:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';
@Component({
template: `
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="username">
<input type="password" formControlName="password">
<button type="submit" [disabled]="loginForm.invalid">Login</button>
</form>
`
})
class LoginComponent {
loginForm: FormGroup;
constructor(private fb: FormBuilder, private router: Router) {
this.loginForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
onSubmit() {
if (this.loginForm.valid) {
// In einer realen App würde hier eine Authentication passieren
this.router.navigate(['/dashboard']);
}
}
}
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let router: Router;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, RouterTestingModule],
declarations: [LoginComponent]
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
router = TestBed.inject(Router);
fixture.detectChanges();
});
it('should navigate to dashboard after successful login', () => {
// Spy auf die navigate-Methode
spyOn(router, 'navigate');
// Formular ausfüllen
const usernameInput = fixture.debugElement.query(By.css('input[formControlName="username"]')).nativeElement;
const passwordInput = fixture.debugElement.query(By.css('input[formControlName="password"]')).nativeElement;
usernameInput.value = 'testuser';
usernameInput.dispatchEvent(new Event('input'));
passwordInput.value = 'password123';
passwordInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
// Formular absenden
const form = fixture.debugElement.query(By.css('form'));
form.triggerEventHandler('submit', null);
// Überprüfen der Navigation
expect(router.navigate).toHaveBeenCalledWith(['/dashboard']);
});
});RxJS ist ein integraler Bestandteil von Angular-Anwendungen und stellt oft eine Herausforderung beim Testen dar. Es gibt verschiedene Ansätze, um RxJS-Code effektiv zu testen.
Der fakeAsync-Wrapper und die tick-Funktion
ermöglichen die Kontrolle über die Zeit in Tests:
import { fakeAsync, tick } from '@angular/core/testing';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
it('should handle delayed observable', fakeAsync(() => {
let result: any;
// Observable mit Verzögerung
of('delayed value').pipe(
delay(1000)
).subscribe(value => {
result = value;
});
// Anfangs kein Ergebnis
expect(result).toBeUndefined();
// Zeit voranschreiten lassen
tick(1000);
// Nach Verzögerung sollte Wert gesetzt sein
expect(result).toBe('delayed value');
}));Der TestScheduler ermöglicht präzise Tests mit der
“Marble Testing”-Syntax:
import { TestScheduler } from 'rxjs/testing';
import { map, filter, debounceTime } from 'rxjs/operators';
describe('RxJS Operators', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should transform values with map operator', () => {
testScheduler.run(({ cold, expectObservable }) => {
const source = cold('a-b-c|', { a: 1, b: 2, c: 3 });
const expected = 'a-b-c|';
const expectedValues = { a: 2, b: 4, c: 6 };
const result = source.pipe(map(x => x * 2));
expectObservable(result).toBe(expected, expectedValues);
});
});
it('should filter values', () => {
testScheduler.run(({ cold, expectObservable }) => {
const source = cold('a-b-c-d-e|', { a: 1, b: 2, c: 3, d: 4, e: 5 });
const expected = 'a---c---e|';
const expectedValues = { a: 1, c: 3, e: 5 };
const result = source.pipe(filter(x => x % 2 === 1));
expectObservable(result).toBe(expected, expectedValues);
});
});
it('should debounce values', () => {
testScheduler.run(({ cold, expectObservable }) => {
const source = cold('a-b-c-----|', { a: 1, b: 2, c: 3 });
const expected = '-----|---(c|)';
const result = source.pipe(debounceTime(5));
expectObservable(result).toBe(expected, { c: 3 });
});
});
});Die Marble-Syntax verwendet spezielle Zeichen, um den zeitlichen Verlauf von Observables darzustellen:
-: Ein Zeitintervall (Frame)a, b, c, …`: Emittierte Werte
(mit Mapping im zweiten Parameter)|: Abschluss des Observables (complete)#: Fehler (error)(): Gleichzeitige Ereignisse gruppieren^: Subscriptionzeitpunkt (nur bei hot Observables)import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { of, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Stellt sicher, dass keine unerwarteten Anfragen übrig sind
});
it('should handle successful data fetch', () => {
const mockData = [{ id: 1, name: 'Item 1' }];
// Observable testen
service.getData().subscribe(data => {
expect(data).toEqual(mockData);
});
// HTTP-Anfrage simulieren
const req = httpMock.expectOne('api/data');
expect(req.request.method).toBe('GET');
req.flush(mockData);
});
it('should handle errors', () => {
let errorResponse: any;
service.getData().pipe(
catchError(error => {
errorResponse = error;
return of(null);
})
).subscribe(() => {
expect(errorResponse.status).toBe(404);
expect(errorResponse.statusText).toBe('Not Found');
});
const req = httpMock.expectOne('api/data');
req.flush('Not found', { status: 404, statusText: 'Not Found' });
});
});Beim Testen von Komponenten, die Observables verwenden:
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';
import { UserService } from './user.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { By } from '@angular/platform-browser';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userServiceSpy: jasmine.SpyObj<UserService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
declarations: [UserListComponent],
providers: [
{ provide: UserService, useValue: spy }
]
}).compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
userServiceSpy = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should display users after loading', fakeAsync(() => {
const mockUsers = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
];
// Service-Antwort mit Verzögerung simulieren
userServiceSpy.getUsers.and.returnValue(of(mockUsers).pipe(delay(500)));
// OnInit auslösen
fixture.detectChanges();
// Überprüfen, dass Loading-Status korrekt ist
expect(component.isLoading).toBe(true);
// Zeit voranschreiten lassen
tick(500);
fixture.detectChanges();
// Überprüfen, dass Loading-Status aktualisiert wurde
expect(component.isLoading).toBe(false);
expect(component.users).toEqual(mockUsers);
// Überprüfen, dass DOM aktualisiert wurde
const userElements = fixture.debugElement.queryAll(By.css('.user-item'));
expect(userElements.length).toBe(2);
expect(userElements[0].nativeElement.textContent).toContain('User 1');
expect(userElements[1].nativeElement.textContent).toContain('User 2');
}));
});Für State-Management mit Subjects:
import { TestBed } from '@angular/core/testing';
import { StateService } from './state.service';
import { skip } from 'rxjs/operators';
describe('StateService', () => {
let service: StateService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [StateService]
});
service = TestBed.inject(StateService);
});
it('should have initial state', () => {
service.state$.subscribe(state => {
expect(state).toEqual({ count: 0, user: null });
});
});
it('should update state', () => {
let currentState: any;
// Skip initial value, nur an Updates interessiert
service.state$.pipe(skip(1)).subscribe(state => {
currentState = state;
});
service.updateCount(5);
expect(currentState).toEqual({ count: 5, user: null });
service.updateUser({ id: 1, name: 'Test User' });
expect(currentState).toEqual({ count: 5, user: { id: 1, name: 'Test User' } });
});
});Für komplexere Observable-Kombinationen:
import { TestScheduler } from 'rxjs/testing';
import { combineLatest, merge, zip, forkJoin } from 'rxjs';
describe('Combination Operators', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should test combineLatest', () => {
testScheduler.run(({ cold, expectObservable }) => {
const a = cold('-a--b--c--|', { a: 1, b: 2, c: 3 });
const b = cold('--x--y--z--|', { x: 10, y: 20, z: 30 });
const result = combineLatest([a, b]).pipe(
map(([aValue, bValue]) => aValue + bValue)
);
const expected = '--A--B-CD-|';
const expectedValues = {
A: 1 + 10, // a + x
B: 2 + 10, // b + x
C: 2 + 20, // b + y
D: 3 + 20, // c + y
E: 3 + 30 // c + z
};
expectObservable(result).toBe(expected, expectedValues);
});
});
});Es ist wichtig, die korrekte Bereinigung von Subscriptions zu testen:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DataComponent } from './data.component';
import { DataService } from './data.service';
import { Subject, of } from 'rxjs';
import { delay } from 'rxjs/operators';
describe('DataComponent', () => {
let component: DataComponent;
let fixture: ComponentFixture<DataComponent>;
let dataSubject: Subject<any>;
let dataServiceSpy: jasmine.SpyObj<DataService>;
beforeEach(async () => {
dataSubject = new Subject();
const spy = jasmine.createSpyObj('DataService', ['getData']);
spy.getData.and.returnValue(dataSubject.asObservable());
await TestBed.configureTestingModule({
declarations: [DataComponent],
providers: [
{ provide: DataService, useValue: spy }
]
}).compileComponents();
fixture = TestBed.createComponent(DataComponent);
component = fixture.componentInstance;
dataServiceSpy = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
fixture.detectChanges();
});
it('should unsubscribe when destroyed', () => {
// Spion für unsubscribe
spyOn(component.subscription, 'unsubscribe').and.callThrough();
// Komponente zerstören
fixture.destroy();
// Überprüfen, dass unsubscribe aufgerufen wurde
expect(component.subscription.unsubscribe).toHaveBeenCalled();
});
});import { TestScheduler } from 'rxjs/testing';
import { switchMap, mergeMap, concatMap, exhaustMap } from 'rxjs/operators';
import { of } from 'rxjs';
describe('Mapping Operators', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should test switchMap behavior', () => {
testScheduler.run(({ cold, hot, expectObservable }) => {
const source = hot('-a-b---c--|');
const innerA = cold('--x--y--|', { x: 'x from a', y: 'y from a' });
const innerB = cold('----z--|', { z: 'z from b' });
const innerC = cold('--w--|', { w: 'w from c' });
const result = source.pipe(
switchMap(value => {
if (value === 'a') return innerA;
if (value === 'b') return innerB;
return innerC;
})
);
// switchMap bricht vorherige innere Observables ab
const expected = '----x--y-z----w--|';
expectObservable(result).toBe(expected, {
x: 'x from a',
y: 'y from a',
z: 'z from b',
w: 'w from c'
});
});
});
});import { TestScheduler } from 'rxjs/testing';
import { catchError, retry } from 'rxjs/operators';
import { of, throwError } from 'rxjs';
describe('Error Handling Operators', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should test catchError', () => {
testScheduler.run(({ cold, expectObservable }) => {
const source = cold('-a-b-#', { a: 1, b: 2 }, 'error message');
const result = source.pipe(
catchError(err => of('fallback'))
);
const expected = '-a-b-f';
const expectedValues = { a: 1, b: 2, f: 'fallback' };
expectObservable(result).toBe(expected, expectedValues);
});
});
it('should test retry', () => {
testScheduler.run(({ cold, expectObservable }) => {
let retryCount = 0;
const source = cold('-a-b-#', { a: 1, b: 2 }, 'error message').pipe(
retry(2),
catchError(err => of('failed after retries'))
);
// Original + 2 Retries, dann Fallback
const expected = '-a-b--a-b--a-b-f';
const expectedValues = { a: 1, b: 2, f: 'failed after retries' };
expectObservable(source).toBe(expected, expectedValues);
});
});
});