Angular verwendet standardmäßig Jasmine als Test-Framework und Karma als Test-Runner. Diese Kombination bildet die Grundlage des Angular-Testökosystems und ist für jeden Angular-Entwickler relevant, der Wert auf Codequalität legt.
Jasmine ist ein Behavior-Driven Development (BDD) Testing-Framework für JavaScript, das eine leserliche, intuitive Syntax bietet, um Tests zu definieren.
describe('Calculator', () => { // Test-Suite
let calculator: Calculator; // Test-Variable
beforeEach(() => { // Setup-Funktion
calculator = new Calculator();
});
it('should add two numbers', () => { // Einzelner Test (Spec)
const result = calculator.add(2, 3);
expect(result).toBe(5); // Assertion
});
it('should subtract two numbers', () => {
const result = calculator.subtract(5, 2);
expect(result).toBe(3);
});
});describeDer describe-Block gruppiert zusammengehörige Tests.
Dies hilft, die Tests zu organisieren und den Kontext zu verstehen.
// Nested describes für bessere Organisation
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
describe('Basic Operations', () => {
it('should add two numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should subtract two numbers', () => {
expect(calculator.subtract(5, 2)).toBe(3);
});
});
describe('Advanced Operations', () => {
it('should calculate power', () => {
expect(calculator.power(2, 3)).toBe(8);
});
it('should calculate square root', () => {
expect(calculator.sqrt(9)).toBe(3);
});
});
});itDer it-Block definiert einen einzelnen Testfall. Der
erste Parameter ist eine String-Beschreibung, die erklärt, was getestet
wird.
it('should return user profile when authenticated', () => {
// Test-Implementierung
});Jasmine bietet mehrere Funktionen für Setup und Teardown:
beforeEach: Wird vor jedem Test innerhalb des
describe-Blocks ausgeführtafterEach: Wird nach jedem Test ausgeführtbeforeAll: Wird einmal vor allen Tests ausgeführtafterAll: Wird einmal nach allen Tests ausgeführtdescribe('Database Service', () => {
let service: DatabaseService;
let connection: Connection;
beforeAll(() => {
// Eine Datenbankverbindung für alle Tests herstellen
connection = createConnection();
});
beforeEach(() => {
// Service für jeden Test neu initialisieren
service = new DatabaseService(connection);
// Testdaten für jeden Test einfügen
service.seed(testData);
});
afterEach(() => {
// Nach jedem Test aufräumen
service.clearAll();
});
afterAll(() => {
// Nach allen Tests Verbindung schließen
connection.close();
});
// Tests...
});expect und MatchersJasmine verwendet expect mit verschiedenen “Matchers”,
um Assertions zu definieren:
// Grundlegende Gleichheit
expect(value).toBe(5); // Strikte Gleichheit (===)
expect(object).toEqual({a: 1}); // Tiefe Objektgleichheit
// Vergleiche
expect(value).toBeLessThan(10);
expect(value).toBeGreaterThan(0);
expect(value).toBeCloseTo(3.14159, 2); // Für Fließkommazahlen mit Genauigkeit
// Wahrheitswerte
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Arrays und Strings
expect(array).toContain('element');
expect(string).toMatch(/pattern/);
expect(array).toHaveSize(3);
// Exceptions
expect(() => { throwingFunction() }).toThrow();
expect(() => { throwingFunction() }).toThrowError('specific error message');
// Negation mit .not
expect(value).not.toBe(6);Jasmine unterstützt verschiedene Ansätze für asynchrone Tests:
Mit done-Callback:
it('should fetch data asynchronously', (done) => {
service.getData().then(data => {
expect(data).toBeDefined();
expect(data.length).toBeGreaterThan(0);
done(); // Signal, dass der Test abgeschlossen ist
});
});Mit Promises (empfohlen):
it('should fetch data asynchronously', async () => {
const data = await service.getData();
expect(data).toBeDefined();
expect(data.length).toBeGreaterThan(0);
});Jasmine bietet leistungsstarke Spies für Mock-Objekte und zur Überwachung von Funktionsaufrufen:
// Eine einzelne Funktion überwachen
spyOn(object, 'method');
spyOn(object, 'method').and.returnValue(10);
spyOn(object, 'method').and.throwError('error message');
spyOn(object, 'method').and.callFake((arg) => arg * 2);
spyOn(object, 'method').and.callThrough(); // Original-Funktion aufrufen
// Ganze Mock-Objekte erstellen
const serviceSpy = jasmine.createSpyObj('ServiceName', ['method1', 'method2']);
serviceSpy.method1.and.returnValue('mocked result');
// Überprüfen von Spy-Aufrufen
expect(object.method).toHaveBeenCalled();
expect(object.method).toHaveBeenCalledTimes(2);
expect(object.method).toHaveBeenCalledWith('arg1', 'arg2');
expect(object.method).not.toHaveBeenCalled();Du kannst eigene Matchers definieren, um Tests lesbarer zu machen:
// Eigenen Matcher definieren
beforeEach(() => {
jasmine.addMatchers({
toBeEvenNumber: function() {
return {
compare: function(actual) {
const result = { pass: actual % 2 === 0 };
if (result.pass) {
result.message = `Expected ${actual} not to be an even number`;
} else {
result.message = `Expected ${actual} to be an even number`;
}
return result;
}
};
}
});
});
// Eigenen Matcher verwenden
it('should detect even numbers', () => {
expect(2).toBeEvenNumber();
expect(3).not.toBeEvenNumber();
});Jasmine ermöglicht das gezielte Ausführen bestimmter Tests:
// Nur diesen einen Test ausführen
fit('focused test', () => {
expect(true).toBe(true);
});
// Nur Tests in dieser Suite ausführen
fdescribe('focused suite', () => {
it('test 1', () => {
expect(true).toBe(true);
});
it('test 2', () => {
expect(false).toBe(false);
});
});
// Diesen Test überspringen
xit('skipped test', () => {
expect(true).toBe(false); // wird nicht ausgeführt
});
// Diese Suite überspringen
xdescribe('skipped suite', () => {
it('skipped test in suite', () => {
expect(true).toBe(false); // wird nicht ausgeführt
});
});Karma ist ein Test-Runner, der von Googles Angular-Team entwickelt wurde. Es führt Tests in echten Browsern aus und bietet eine Infrastruktur für kontinuierliches Testen.
Die Karma-Konfiguration wird in karma.conf.js
definiert:
module.exports = function(config) {
config.set({
// Basis-Pfad, der für alle relativen Pfade verwendet wird
basePath: '',
// Zu verwendende Frameworks
frameworks: ['jasmine', '@angular-devkit/build-angular'],
// Liste der zu ladenden Plugins
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
// Client-Konfiguration
client: {
clearContext: false, // Jasmine spec runner output bleibt erhalten
jasmine: {
// Jasmine-spezifische Einstellungen (z.B. Timeout)
timeoutInterval: 10000
}
},
// Coverage-Reporter-Konfiguration
coverageReporter: {
dir: require('path').join(__dirname, './coverage/my-app'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
// Zu verwendende Reporter
reporters: ['progress', 'kjhtml'],
// Webserver-Port
port: 9876,
// Farben in der Ausgabe aktivieren
colors: true,
// Log-Level (mögliche Werte: LOG_DISABLE, LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG)
logLevel: config.LOG_INFO,
// Automatisches Neuladen bei Dateiänderungen
autoWatch: true,
// Zu startende Browser
browsers: ['Chrome'],
// Karma kann als Daemon im Hintergrund laufen
// true: Karma startet, führt Tests aus und beendet sich
// false: Karma läuft und führt Tests bei Änderungen aus
singleRun: false,
// Bei Dateiänderungen neu starten
restartOnFileChange: true,
// Zuschausmodus (keine Tests werden ausgeführt, nur Browser-Fenster erscheinen)
watch: false
});
};Karma kann Tests in verschiedenen Browsern ausführen:
// Unterschiedliche Browser für Tests
browsers: ['Chrome', 'Firefox', 'Safari', 'Edge'],
// Headless-Browser für CI/CD-Pipelines
browsers: ['ChromeHeadless', 'FirefoxHeadless'],
// Benutzerdefinierte Browser-Konfiguration
customLaunchers: {
ChromeDebugging: {
base: 'Chrome',
flags: ['--remote-debugging-port=9333']
},
ChromeNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
browsers: ['ChromeNoSandbox'],Code-Coverage-Berichte zeigen, wie viel des Codes durch Tests abgedeckt ist:
// Coverage-Reporter konfigurieren
coverageReporter: {
dir: 'coverage/',
reporters: [
{ type: 'html', subdir: 'html' },
{ type: 'lcov', subdir: 'lcov' },
{ type: 'text-summary' },
{ type: 'cobertura', subdir: '.', file: 'cobertura.xml' }
],
check: {
global: {
statements: 80,
branches: 70,
functions: 80,
lines: 80
}
}
},Um Tests schneller auszuführen, können sie parallel in mehreren Browsern laufen:
// Parallel in mehreren Browser-Instanzen testen
concurrency: 3, // Anzahl paralleler Browser-InstanzenIn einem Angular-Projekt sind Karma-Befehle in
package.json als Scripts definiert:
# Normale Testausführung
ng test
# Einmalige Testausführung (z.B. für CI/CD)
ng test --watch=false
# Tests mit Code-Coverage
ng test --code-coverage
# Tests in einem Headless-Browser
ng test --browsers=ChromeHeadless
# Bestimmte Test-Dateien ausführen
ng test --include=src/app/my-component/*.spec.tsEin großer Vorteil von Karma ist die Möglichkeit, Tests direkt im Browser zu debuggen:
ng testUm einen einzelnen Test zu debuggen, kannst du fdescribe
oder fit verwenden, oder die Debugging-URL anpassen:
http://localhost:9876/debug.html?spec=MyComponent%20should%20create
Karma kann leicht in CI/CD-Pipelines integriert werden:
# Beispiel für GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16.x'
- run: npm ci
- run: npm test -- --watch=false --browsers=ChromeHeadless# Linux-Abhängigkeiten für Chrome Headless
sudo apt-get install -y libgbm-dev
# Karma konfigurieren, um ohne Sandbox zu laufen
browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu']
}
}// Timeout-Werte erhöhen
client: {
jasmine: {
timeoutInterval: 30000 // 30 Sekunden
}
},
browserDisconnectTimeout: 10000,
browserNoActivityTimeout: 60000,// Debug-Ausgabe aktivieren
logLevel: config.LOG_DEBUG,
// Spezielle Browser-Konfiguration für CI
browsers: process.env.CI ? ['ChromeHeadlessCI'] : ['Chrome'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu', '--disable-web-security']
}
}Die Integration von Jasmine und Karma in Angular-Projekten erfolgt automatisch durch die Angular CLI.
Angular CLI generiert automatisch Test-Dateien
(.spec.ts) für Komponenten, Services usw.:
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserService]
});
service = TestBed.inject(UserService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
// Weitere Tests...
});# Alle Tests ausführen
ng test
# Bestimmte Tests ausführen
ng test --include=src/app/auth
# Bestimmte Test-Suites ausführen
ng test --test-name-pattern="UserService"// Logische Gruppierung in describe-Blöcken
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
// Setup-Code
});
describe('authentication', () => {
it('should authenticate valid users', () => {
// Test-Code
});
it('should reject invalid credentials', () => {
// Test-Code
});
});
describe('authorization', () => {
it('should check user permissions', () => {
// Test-Code
});
});
});// TestBed-Konfiguration wiederverwenden
let originalTimeout: number;
// Einmalig konfigurieren
beforeAll(() => {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
TestBed.configureTestingModule({
// Konfiguration
});
});
// Zurücksetzen nach allen Tests
afterAll(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
// Für jeden Test eine neue Instanz erstellen
beforeEach(() => {
service = TestBed.inject(AuthService);
});// Mock-Provider für HTTP-Anfragen
providers: [
{
provide: HttpClient,
useValue: jasmine.createSpyObj('HttpClient', ['get', 'post'])
}
]
// Weniger tiefe Komponentenbäume mit NO_ERRORS_SCHEMA
TestBed.configureTestingModule({
declarations: [ParentComponent], // Deklariere nur die zu testende Komponente
schemas: [NO_ERRORS_SCHEMA] // Ignoriere unbekannte Element/Attribute
});Obwohl Jasmine und Karma die Standardtools für Angular-Tests sind, gibt es Alternativen:
Jest bietet eine All-in-One-Lösung (Test-Runner und Framework) mit schnellerer Ausführung:
# Jest in einem Angular-Projekt einrichten
ng add @angular-builders/jest// jest.config.js
module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
globalSetup: 'jest-preset-angular/global-setup',
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/'],
testMatch: ['**/*.spec.ts']
};# Mit Angular Builders
ng test
# Direkt mit Jest
npx jest
npx jest --watch
npx jest --coverageCypress kann neben E2E-Tests auch für Komponententests verwendet werden:
# Cypress für Komponententests einrichten
ng add @cypress/schematic// src/app/button/button.component.cy.ts
import { ButtonComponent } from './button.component';
describe('ButtonComponent', () => {
it('should render button with text', () => {
cy.mount(ButtonComponent, {
componentProperties: {
text: 'Click me',
disabled: false
}
});
cy.get('button').should('contain.text', 'Click me');
cy.get('button').should('not.be.disabled');
cy.get('button').click();
});
});# UI-Modus starten
npx cypress open --component
# Headless-Modus
npx cypress run --component