26 Jasmine und Karma in Angular-Tests

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.

26.1 Jasmine: Das Behavior-Driven Testing-Framework

Jasmine ist ein Behavior-Driven Development (BDD) Testing-Framework für JavaScript, das eine leserliche, intuitive Syntax bietet, um Tests zu definieren.

26.1.1 Grundstruktur eines Jasmine-Tests

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

26.1.2 Jasmine-Schlüsselkomponenten

26.1.2.1 Test-Suites mit describe

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

26.1.2.2 Einzelne Tests mit it

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

26.1.2.3 Setup und Teardown

Jasmine bietet mehrere Funktionen für Setup und Teardown:

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

26.1.2.4 Erwartungen mit expect und Matchers

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

26.1.2.5 Asynchrone Tests

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

26.1.2.6 Spies für Mocking und Test-Doubles

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

26.1.3 Erweiterte Jasmine-Funktionen

26.1.3.1 Benutzerdefinierte Matchers

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

26.1.3.2 Filtern und Fokussieren von Tests

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

26.2 Karma: Der Test-Runner

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.

26.2.1 Funktionsweise von Karma

  1. Startet einen Webserver, der die Testdateien, die Anwendung und Jasmine bereitstellt
  2. Öffnet Browser und verbindet sie mit dem Server
  3. Führt die Tests in den Browsern aus
  4. Sammelt Ergebnisse und gibt sie an die Konsole aus

26.2.2 Karma-Konfiguration

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

26.2.3 Wichtige Karma-Konfigurationsoptionen

26.2.3.1 Browser-Konfiguration

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'],

26.2.3.2 Coverage-Reports

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
    }
  }
},

26.2.3.3 Parallele Test-Ausführung

Um Tests schneller auszuführen, können sie parallel in mehreren Browsern laufen:

// Parallel in mehreren Browser-Instanzen testen
concurrency: 3, // Anzahl paralleler Browser-Instanzen

26.2.4 Karma-Befehle in Angular-Projekten

In 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.ts

26.2.5 Test-Debugging mit Karma

Ein großer Vorteil von Karma ist die Möglichkeit, Tests direkt im Browser zu debuggen:

  1. Starte den Test-Runner mit ng test
  2. Öffne den Browser, der von Karma gestartet wurde
  3. Klicke auf “Debug” im Karma-Fenster
  4. Öffne die Browser-Entwicklertools (F12)
  5. Setze Breakpoints in deinem Code
  6. Aktualisiere die Debug-Seite

Um 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

26.2.6 Integration mit CI/CD-Pipelines

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

26.2.7 Häufige Probleme und Lösungen

26.2.7.1 Karma startet Browser nicht

# 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']
  }
}

26.2.7.2 Timeouts bei langsamen Tests

// Timeout-Werte erhöhen
client: {
  jasmine: {
    timeoutInterval: 30000 // 30 Sekunden
  }
},
browserDisconnectTimeout: 10000,
browserNoActivityTimeout: 60000,

26.2.7.3 Tests laufen lokal, aber nicht in CI

// 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']
  }
}

26.3 Jasmine und Karma in Angular-Projekten

Die Integration von Jasmine und Karma in Angular-Projekten erfolgt automatisch durch die Angular CLI.

26.3.1 Standardmäßige Test-Struktur

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

26.3.2 Tests ausführen

# 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"

26.3.3 Best Practices für Jasmine und Karma in Angular

26.3.3.1 Effiziente Test-Organisation

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

26.3.3.2 Test-Performance verbessern

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

26.3.3.3 Integrationstests effizienter gestalten

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

26.4 Alternativen zu Jasmine und Karma

Obwohl Jasmine und Karma die Standardtools für Angular-Tests sind, gibt es Alternativen:

26.4.1 Jest als Alternative

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

26.4.1.1 Jest-Konfiguration:

// 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']
};

26.4.1.2 Jest-Tests ausführen:

# Mit Angular Builders
ng test

# Direkt mit Jest
npx jest
npx jest --watch
npx jest --coverage

26.4.2 Cypress für Komponententests

Cypress kann neben E2E-Tests auch für Komponententests verwendet werden:

# Cypress für Komponententests einrichten
ng add @cypress/schematic

26.4.2.1 Cypress-Komponententest:

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

26.4.2.2 Cypress-Komponententests ausführen:

# UI-Modus starten
npx cypress open --component

# Headless-Modus
npx cypress run --component