25 Jasmine und Karma für Angular-Tests

25.1 Das Testing-Ökosystem verstehen

Angular-Anwendungen werden nicht nur entwickelt, sondern auch kontinuierlich getestet. Automated Testing ist kein optionales Add-on, sondern integraler Bestandteil professioneller Softwareentwicklung. Das Angular-Team hat Jasmine als Testing-Framework und Karma als Test-Runner standardisiert, eine bewährte Kombination, die sich über Jahre in der Produktion etabliert hat.

Diese Toolchain löst ein fundamentales Problem: Wie testet man komplexe, zustandsbehaftete Browser-Anwendungen automatisiert, reproduzierbar und in verschiedenen Umgebungen? Die Antwort liegt in der Arbeitsteilung. Jasmine definiert, wie Tests geschrieben werden – die Syntax, die Assertions, die Mocking-Mechanismen. Karma kümmert sich um die Ausführung – Browser-Management, File-Watching, Reporting.

Der Workflow ist klar: Tests werden in Jasmine-Syntax geschrieben, Karma lädt diese Tests in echte Browser, führt sie aus und sammelt die Ergebnisse. Diese Architektur ermöglicht Cross-Browser-Testing ohne manuelle Interaktion.

25.2 Jasmine: Behavior-Driven Testing

Jasmine folgt dem Behavior-Driven Development-Ansatz. Tests beschreiben Verhalten in natürlicher Sprache, nicht technische Implementierungsdetails. Ein Calculator wird nicht “getestet”, sondern “sollte zwei Zahlen addieren können”. Diese Formulierung macht Tests lesbarer und dokumentiert gleichzeitig die erwartete Funktionalität.

Die grundlegende Struktur jedes Jasmine-Tests:

describe('Calculator', () => {
  let calculator: Calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  it('should add two numbers correctly', () => {
    const result = calculator.add(2, 3);
    expect(result).toBe(5);
  });

  it('should handle negative numbers', () => {
    const result = calculator.add(-5, 3);
    expect(result).toBe(-2);
  });
});

Der describe-Block gruppiert zusammengehörige Tests zu einer Suite. Die String-Beschreibung identifiziert den Testgegenstand. Innerhalb der Suite definiert it einzelne Spezifikationen – konkrete, testbare Aussagen über das Verhalten. Die expect-Funktion mit Matchers formuliert Assertions – Bedingungen, die erfüllt sein müssen.

Die beforeEach-Funktion läuft vor jedem Test. Sie stellt sicher, dass jeder Test mit frischem State beginnt. Dies verhindert Interferenzen – ein fehlgeschlagener Test beeinflusst nicht nachfolgende Tests. Die Isolation ist essentiell für zuverlässige Testsuiten.

25.3 Test-Organisation und Struktur

Komplexe Komponenten erfordern strukturierte Tests. Jasmine unterstützt verschachtelte describe-Blöcke für hierarchische Organisation:

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  describe('Authentication', () => {
    it('should login with valid credentials', () => {
      const credentials = { username: 'user', password: 'pass' };
      
      service.login(credentials).subscribe(result => {
        expect(result.token).toBeDefined();
        expect(result.user.username).toBe('user');
      });
      
      const req = httpMock.expectOne('/api/auth/login');
      req.flush({ token: 'abc123', user: { username: 'user' } });
    });

    it('should reject invalid credentials', () => {
      const credentials = { username: 'user', password: 'wrong' };
      
      service.login(credentials).subscribe(
        () => fail('should have failed'),
        error => expect(error.status).toBe(401)
      );
      
      const req = httpMock.expectOne('/api/auth/login');
      req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });
    });
  });

  describe('User Management', () => {
    it('should fetch user profile', () => {
      service.getProfile(123).subscribe(user => {
        expect(user.id).toBe(123);
        expect(user.email).toBeDefined();
      });
      
      const req = httpMock.expectOne('/api/users/123');
      req.flush({ id: 123, email: 'user@example.com' });
    });
  });

  afterEach(() => {
    httpMock.verify();
  });
});

Die verschachtelte Struktur spiegelt funktionale Gruppierung wider. Authentication-Tests sind separiert von User-Management-Tests. Jede Gruppe kann eigene Setup-Logik haben, während gemeinsame Initialisierung im äußeren beforeEach bleibt.

25.3.1 Setup und Teardown-Hooks

Jasmine bietet vier Lifecycle-Hooks mit unterschiedlichen Ausführungszeitpunkten:

Hook Ausführung Verwendung
beforeEach Vor jedem Test Test-Daten initialisieren, Mocks aufsetzen
afterEach Nach jedem Test Aufräumen, Subscriptions beenden
beforeAll Einmal vor allen Tests Teure Ressourcen erstellen (DB-Connections)
afterAll Einmal nach allen Tests Ressourcen freigeben

Die Wahl zwischen beforeEach und beforeAll hat Performance-Implikationen. beforeAll ist schneller, da Setup nur einmal läuft. Tests teilen sich jedoch State, was zu Interferenzen führen kann. beforeEach garantiert Isolation, kostet aber Performance bei teuren Operations.

describe('Database Operations', () => {
  let db: Database;
  let connection: Connection;

  beforeAll(async () => {
    connection = await createConnection({
      type: 'sqlite',
      database: ':memory:'
    });
    await connection.synchronize();
  });

  beforeEach(() => {
    db = new Database(connection);
  });

  afterEach(async () => {
    await connection.query('DELETE FROM users');
  });

  afterAll(async () => {
    await connection.close();
  });

  it('should insert user', async () => {
    const user = await db.createUser({ name: 'John' });
    expect(user.id).toBeDefined();
  });

  it('should find user by id', async () => {
    const created = await db.createUser({ name: 'Jane' });
    const found = await db.findUser(created.id);
    expect(found.name).toBe('Jane');
  });
});

Die Connection wird einmal erstellt und geteilt. Zwischen Tests werden Daten gelöscht, um Isolation zu gewährleisten. Nach allen Tests wird die Connection geschlossen.

25.4 Matchers für präzise Assertions

Jasmine’s Matcher-System bietet spezifische Assertions für verschiedene Datentypen und Szenarien. Die Wahl des richtigen Matchers macht Tests ausdrucksstärker und Fehlermeldungen klarer.

Grundlegende Gleichheitsprüfung unterscheidet zwischen Identität und struktureller Gleichheit:

// Identität: Strikte Gleichheit (===)
expect(value).toBe(5);
expect(reference1).toBe(reference2);

// Strukturelle Gleichheit: Tiefe Objektvergleiche
expect(user).toEqual({ id: 1, name: 'John' });
expect(array).toEqual([1, 2, 3]);

toBe prüft Referenz-Gleichheit. Zwei Objekte mit identischem Inhalt sind nicht toBe gleich, wenn sie unterschiedliche Instanzen sind. toEqual vergleicht Inhalte rekursiv – ideal für Objekte und Arrays.

Numerische Vergleiche unterstützen Präzisionsprobleme bei Floating-Point-Arithmetik:

expect(value).toBeGreaterThan(10);
expect(value).toBeLessThanOrEqual(100);

// Floating-Point-Vergleich mit Toleranz
expect(Math.PI).toBeCloseTo(3.14159, 5); // 5 Dezimalstellen

toBeCloseTo ist essentiell für numerische Berechnungen. Floating-Point-Arithmetik ist inhärent unpräzise – 0.1 + 0.2 ergibt nicht exakt 0.3. Der zweite Parameter definiert die erwartete Genauigkeit.

Wahrheitswert-Tests decken JavaScript’s komplexe Truthiness ab:

expect(value).toBeTruthy();    // != false, 0, '', null, undefined, NaN
expect(value).toBeFalsy();     // == false, 0, '', null, undefined, NaN
expect(value).toBeNull();      // === null
expect(value).toBeUndefined(); // === undefined
expect(value).toBeDefined();   // !== undefined

Die Unterscheidung ist subtil aber wichtig. toBeTruthy() matched viele Werte, toBeDefined() nur Nicht-Undefined. Die Wahl kommuniziert Intention – erwartet der Test einen konkreten Wert oder nur “irgendwas außer undefined”?

Collection-Tests prüfen Inhalte ohne exakte Reihenfolge:

expect(array).toContain('element');
expect(array).toHaveSize(5);
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');

Exception-Tests verifizieren Error-Handling:

expect(() => throwingFunction()).toThrow();
expect(() => throwingFunction()).toThrowError('specific message');
expect(() => throwingFunction()).toThrowError(TypeError);

Die Funktion wird als Callback übergeben, nicht direkt aufgerufen. Jasmine fängt die Exception und validiert sie. Ohne Callback würde die Exception den Test abbrechen.

Negation invertiert jeden Matcher:

expect(value).not.toBe(6);
expect(array).not.toContain('missing');
expect(() => safeFunction()).not.toThrow();

25.5 Asynchrone Tests meistern

Angular-Anwendungen sind asynchron. HTTP-Requests, Timers, Promises, Observables – alles läuft außerhalb des synchronen Flows. Tests müssen diese Asynchronität handhaben, sonst schlagen sie fehl oder produzieren false positives.

Jasmine bietet drei Mechanismen für asynchrone Tests. Der klassische done-Callback:

it('should load user data', (done) => {
  userService.getUser(123).subscribe(user => {
    expect(user.id).toBe(123);
    expect(user.name).toBeDefined();
    done();
  });
});

Der done-Parameter signalisiert Jasmine, dass der Test asynchron ist. Jasmine wartet, bis done() aufgerufen wird. Fehlt der Aufruf, timeoutet der Test. Passiert eine Exception vor done(), schlägt der Test fehl.

Die moderne Promise-basierte Syntax ist eleganter:

it('should load user data', async () => {
  const user = await userService.getUser(123).toPromise();
  expect(user.id).toBe(123);
  expect(user.name).toBeDefined();
});

Das async-Keyword macht die Test-Funktion zu einer async Function. Jasmine erkennt dies und wartet auf Promise-Resolution. Exceptions werden automatisch gefangen und führen zum Test-Failure.

Für Observable-Tests bietet Angular’s fakeAsync und tick:

import { fakeAsync, tick } from '@angular/core/testing';

it('should debounce search input', fakeAsync(() => {
  let result: string;
  
  searchService.search('query').subscribe(r => result = r);
  
  tick(300); // Simuliere 300ms Zeitverlauf
  
  expect(result).toBe('results');
}));

fakeAsync erstellt eine Zone mit virtueller Zeit. tick() springt die Zeit vorwärts, ohne real zu warten. Dies beschleunigt Tests mit Timeouts oder Debouncing drastisch.

25.6 Spies für Isolation und Mocking

Unit-Tests sollen die zu testende Unit isolieren. Dependencies werden durch Test-Doubles ersetzt – Objekte, die das Interface implementieren, aber kontrolliertes Verhalten zeigen. Jasmine’s Spy-System implementiert dies elegant.

Ein Spy auf einer existierenden Methode:

const userService = new UserService(httpClient);

spyOn(userService, 'getUser').and.returnValue(
  of({ id: 123, name: 'John' })
);

component.loadUser(123);

expect(userService.getUser).toHaveBeenCalledWith(123);
expect(component.user.name).toBe('John');

spyOn ersetzt die Methode durch einen Spy. Das and.returnValue kontrolliert den Return-Wert. Der Test verifiziert sowohl den Aufruf als auch das Ergebnis.

Verschiedene Spy-Konfigurationen für verschiedene Szenarien:

// Einfacher Return-Wert
spyOn(service, 'getData').and.returnValue(testData);

// Exception werfen
spyOn(service, 'failingMethod').and.throwError('Network error');

// Custom-Implementierung
spyOn(service, 'transform').and.callFake(input => input.toUpperCase());

// Original-Methode aufrufen (für Beobachtung ohne Ersetzung)
spyOn(service, 'logActivity').and.callThrough();

Für komplette Mock-Objekte nutzt createSpyObj:

const httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);

httpSpy.get.and.returnValue(of({ data: 'mocked' }));
httpSpy.post.and.returnValue(of({ success: true }));

const service = new UserService(httpSpy);

Das Mock-Objekt implementiert das Interface mit Spies für jede Methode. Die Service-Instanz erhält den Mock statt des echten HttpClient. Der Test bleibt isoliert – keine echten HTTP-Requests.

Spy-Verifikation prüft Aufruf-Details:

expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(3);
expect(spy).toHaveBeenCalledWith('arg1', 'arg2');
expect(spy).not.toHaveBeenCalledWith('wrongArg');
expect(spy).toHaveBeenCalledBefore(otherSpy);

// Zugriff auf Aufruf-Details
expect(spy.calls.count()).toBe(2);
expect(spy.calls.argsFor(0)).toEqual(['first', 'call']);
expect(spy.calls.mostRecent().args).toEqual(['last', 'call']);

Diese Assertions verifizieren nicht nur dass eine Methode aufgerufen wurde, sondern wie oft, mit welchen Parametern und in welcher Reihenfolge.

25.7 Benutzerdefinierte Matchers

Standard-Matchers decken Common Cases ab. Domain-spezifische Assertions profitieren von Custom Matchers, die Tests lesbarer und wiederverwendbarer machen:

beforeEach(() => {
  jasmine.addMatchers({
    toBeValidEmail: () => ({
      compare: (actual: string) => {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        const pass = emailRegex.test(actual);
        
        return {
          pass,
          message: pass
            ? `Expected ${actual} not to be a valid email`
            : `Expected ${actual} to be a valid email`
        };
      }
    }),
    
    toBeWithinRange: () => ({
      compare: (actual: number, min: number, max: number) => {
        const pass = actual >= min && actual <= max;
        
        return {
          pass,
          message: pass
            ? `Expected ${actual} not to be within range ${min}-${max}`
            : `Expected ${actual} to be within range ${min}-${max}, but was ${actual}`
        };
      }
    })
  });
});

it('should validate email addresses', () => {
  expect('user@example.com').toBeValidEmail();
  expect('invalid-email').not.toBeValidEmail();
});

it('should validate number ranges', () => {
  expect(50).toBeWithinRange(0, 100);
  expect(150).not.toBeWithinRange(0, 100);
});

Custom Matchers kapseln komplexe Validierungslogik. Die Fehlermeldungen sind spezifisch und hilfreich. Tests werden deklarativer und ausdrucksstärker.

25.8 Test-Fokussierung und Filtering

Während der Entwicklung ist es oft nützlich, nur spezifische Tests auszuführen. Jasmine’s Fokussierungs-Features beschleunigen den Feedback-Loop:

// Nur dieser Test läuft
fit('focused test', () => {
  expect(true).toBe(true);
});

// Alle anderen Tests werden ignoriert
it('ignored test', () => {
  expect(false).toBe(false); // wird übersprungen
});

// Nur Tests in dieser Suite laufen
fdescribe('focused suite', () => {
  it('runs', () => {
    expect(true).toBe(true);
  });
  
  it('also runs', () => {
    expect(true).toBe(true);
  });
});

// Suite überspringen
xdescribe('disabled suite', () => {
  it('skipped', () => {
    expect(false).toBe(false); // wird nicht ausgeführt
  });
});

// Einzelnen Test überspringen
xit('disabled test', () => {
  expect(false).toBe(false); // wird nicht ausgeführt
});

Das f-Präfix (focus) aktiviert selektive Ausführung. Das x-Präfix (exclude) deaktiviert Tests. Diese Markierungen sollten temporär sein – committed Code sollte keine focused Tests enthalten, da dies andere Tests verschweigt.

25.9 Karma: Der Test-Orchestrator

Jasmine definiert Tests, Karma führt sie aus. Diese Trennung ermöglicht Flexibilität – Jasmine-Tests können in verschiedenen Umgebungen laufen, Karma kann verschiedene Testing-Frameworks orchestrieren.

Karma’s Architektur basiert auf einem Webserver, der Test-Code und Anwendungscode in Browser lädt. Die Browser werden zu Clients, die mit dem Karma-Server kommunizieren. Der Server sammelt Ergebnisse und präsentiert sie dem Entwickler.

Der Workflow unterstützt Watch-Mode. Karma beobachtet Dateien, erkennt Änderungen und führt Tests automatisch erneut aus. Der Entwickler erhält sofortiges Feedback ohne manuelle Test-Ausführung.

25.10 Karma-Konfiguration verstehen

Die Datei karma.conf.js konfiguriert jeden Aspekt des Test-Runners:

module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage'),
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    
    client: {
      clearContext: false,
      jasmine: {
        timeoutInterval: 10000,
        random: false
      }
    },
    
    jasmineHtmlReporter: {
      suppressAll: false,
      suppressFailed: false
    },
    
    coverageReporter: {
      dir: require('path').join(__dirname, './coverage'),
      subdir: '.',
      reporters: [
        { type: 'html' },
        { type: 'text-summary' },
        { type: 'lcovonly' }
      ],
      check: {
        global: {
          statements: 80,
          branches: 75,
          functions: 80,
          lines: 80
        }
      }
    },
    
    reporters: ['progress', 'kjhtml', 'coverage'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    restartOnFileChange: true
  });
};

Die Konfiguration steuert Browser-Auswahl, Reporter-Ausgabe, Coverage-Thresholds und mehr. Der frameworks-Array definiert verwendete Testing-Frameworks. Der plugins-Array registriert Karma-Plugins für Browser-Launcher, Reporter und Build-Integration.

25.10.1 Browser-Konfiguration für verschiedene Szenarien

Karma unterstützt alle gängigen Browser. Die Wahl hängt vom Kontext ab – lokale Entwicklung, CI/CD-Pipeline, Cross-Browser-Testing:

// Lokale Entwicklung: Sichtbare Browser
browsers: ['Chrome'],

// CI/CD: Headless Browser
browsers: ['ChromeHeadless'],

// Cross-Browser-Testing
browsers: ['Chrome', 'Firefox', 'Safari'],

// Custom Launcher für spezielle Flags
customLaunchers: {
  ChromeHeadlessCI: {
    base: 'ChromeHeadless',
    flags: [
      '--no-sandbox',
      '--disable-gpu',
      '--disable-dev-shm-usage'
    ]
  },
  ChromeDebug: {
    base: 'Chrome',
    flags: ['--remote-debugging-port=9333']
  }
},

browsers: process.env.CI ? ['ChromeHeadlessCI'] : ['Chrome']

Die --no-sandbox-Flag ist oft nötig in Docker-Containern oder CI-Umgebungen mit eingeschränkten Berechtigungen. Die --remote-debugging-port-Flag ermöglicht externes Debugging während der Test-Ausführung.

25.10.2 Coverage-Reports und Thresholds

Code-Coverage misst, welcher Anteil des Codes durch Tests abgedeckt ist. Karma integriert Istanbul für Coverage-Tracking:

coverageReporter: {
  dir: 'coverage/',
  reporters: [
    { type: 'html', subdir: 'html' },
    { type: 'lcovonly', subdir: '.' },
    { type: 'text-summary' },
    { type: 'json', subdir: '.', file: 'coverage.json' }
  ],
  check: {
    global: {
      statements: 80,
      branches: 70,
      functions: 80,
      lines: 80
    },
    each: {
      statements: 60,
      branches: 50
    }
  }
}

Die check-Konfiguration definiert minimale Coverage-Prozentsätze. Unterschreiten Tests diese Thresholds, schlägt der Build fehl. Dies erzwingt Mindest-Testabdeckung in CI/CD-Pipelines.

Coverage-Typen haben unterschiedliche Bedeutungen:

Metrik Bedeutung Interpretation
Statements Ausgeführte Code-Zeilen Grundlegende Abdeckung
Branches If/else-Zweige durchlaufen Edge Cases getestet
Functions Aufgerufene Funktionen API-Surface getestet
Lines Physische Zeilen ausgeführt Ähnlich Statements

Hohe Coverage garantiert nicht gute Tests – sie misst nur Ausführung, nicht Qualität. Niedrige Coverage weist jedoch auf ungetestete Bereiche hin.

25.11 Karma in der Praxis

Die Angular CLI integriert Karma nahtlos. Standard-Befehle abstrahieren Karma-Details:

# Tests ausführen im Watch-Mode
ng test

# Einmalige Ausführung für CI/CD
ng test --watch=false --browsers=ChromeHeadless

# Mit Coverage-Report
ng test --code-coverage

# Spezifische Dateien testen
ng test --include='src/app/auth/**/*.spec.ts'

# Mit Source Maps für Debugging
ng test --source-map

Die CLI konfiguriert Karma automatisch basierend auf angular.json. Custom-Konfiguration in karma.conf.js überschreibt Defaults.

25.11.1 Test-Debugging im Browser

Ein Vorteil von Karma ist natives Browser-Debugging. Der Test-Runner öffnet einen Browser mit Debug-Interface:

  1. ng test startet Karma und öffnet Browser
  2. Click “Debug” im Karma-Interface
  3. Browser-DevTools öffnen (F12)
  4. Breakpoints im Test-Code setzen
  5. Seite refreshen, Execution stoppt an Breakpoints

Die Debug-URL kann modifiziert werden für selektive Ausführung:

http://localhost:9876/debug.html?spec=UserService%20should%20authenticate

Dies lädt nur Tests, die “UserService should authenticate” matchen. Kombiniert mit Breakpoints ermöglicht dies fokussiertes Debugging komplexer Tests.

25.11.2 CI/CD-Integration

Karma ist CI/CD-ready. Die Konfiguration unterscheidet zwischen lokaler Entwicklung und CI-Umgebung:

module.exports = function(config) {
  const isCI = process.env.CI === 'true';
  
  config.set({
    browsers: isCI ? ['ChromeHeadlessCI'] : ['Chrome'],
    singleRun: isCI,
    autoWatch: !isCI,
    
    customLaunchers: {
      ChromeHeadlessCI: {
        base: 'ChromeHeadless',
        flags: [
          '--no-sandbox',
          '--disable-gpu',
          '--disable-dev-shm-usage',
          '--disable-software-rasterizer',
          '--disable-extensions'
        ]
      }
    },
    
    browserDisconnectTimeout: 10000,
    browserNoActivityTimeout: 60000,
    captureTimeout: 60000
  });
};

Die Environment-Variable CI steuert das Verhalten. In CI laufen Tests headless, einmalig, mit erhöhten Timeouts für langsamere Umgebungen.

GitHub Actions Beispiel:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install Dependencies
        run: npm ci
      
      - name: Run Tests
        run: npm test -- --watch=false --browsers=ChromeHeadlessCI
      
      - name: Upload Coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

Die Pipeline installiert Dependencies, führt Tests aus und uploaded Coverage-Reports. Die --watch=false-Flag beendet Karma nach Test-Completion.

25.12 Troubleshooting häufiger Probleme

Karma’s Abhängigkeit von echten Browsern führt zu plattformspezifischen Problemen. Typische Issues und Lösungen:

Browser startet nicht in CI:

customLaunchers: {
  ChromeHeadlessCI: {
    base: 'ChromeHeadless',
    flags: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-gpu',
      '--disable-dev-shm-usage'
    ]
  }
}

Docker-Container und CI-Umgebungen haben oft eingeschränkte Sandbox-Permissions. Die --no-sandbox-Flag umgeht dies. Die --disable-dev-shm-usage-Flag verhindert /dev/shm Memory Issues in Containern.

Timeout-Fehler bei langsamen Tests:

client: {
  jasmine: {
    timeoutInterval: 30000  // 30 Sekunden
  }
},
browserDisconnectTimeout: 20000,
browserNoActivityTimeout: 60000

Langsame CI-Maschinen oder komplexe Tests benötigen höhere Timeouts. Zu hohe Werte verschleiern jedoch echte Probleme – Tests sollten schnell sein.

Tests laufen lokal, scheitern in CI:

logLevel: config.LOG_DEBUG,

browsers: process.env.CI ? ['ChromeHeadlessCI'] : ['Chrome'],

customLaunchers: {
  ChromeHeadlessCI: {
    base: 'ChromeHeadless',
    flags: [
      '--no-sandbox',
      '--disable-gpu',
      '--window-size=1920,1080'
    ]
  }
}

Debug-Logging offenbart oft Unterschiede. Die --window-size-Flag stellt konsistente Viewport-Größen sicher, wichtig für UI-Tests.

25.13 Alternativen zum Standard-Stack

Jasmine und Karma sind Angular’s Defaults, aber nicht die einzigen Optionen. Jest gewinnt an Popularität durch Speed und Developer Experience:

ng add @angular-builders/jest

Jest kombiniert Test-Framework und Runner in einem Tool. Es läuft in Node.js statt echten Browsern, was drastisch schneller ist. Die Snapshot-Testing-Features vereinfachen UI-Regression-Tests.

Ein Jest-Test sieht Jasmine ähnlich:

describe('UserService', () => {
  let service: UserService;
  
  beforeEach(() => {
    service = new UserService();
  });
  
  it('should create user', () => {
    const user = service.createUser({ name: 'John' });
    expect(user).toMatchSnapshot();
  });
});

Die Snapshot wird beim ersten Lauf gespeichert. Folgende Runs vergleichen gegen die Snapshot. Änderungen am Output werden als Failure markiert, bis die Snapshot aktualisiert wird.

Cypress erweitert sich von E2E zu Component Testing:

import { UserCardComponent } from './user-card.component';

describe('UserCardComponent', () => {
  it('should display user information', () => {
    cy.mount(UserCardComponent, {
      componentProperties: {
        user: { name: 'John', email: 'john@example.com' }
      }
    });
    
    cy.get('.user-name').should('contain', 'John');
    cy.get('.user-email').should('contain', 'john@example.com');
  });
});

Cypress rendert Komponenten in einem echten Browser, ähnlich Karma. Die API ist jedoch anders – Cypress verwendet Chaining statt Jasmine’s Expect-Syntax. Die Time-Travel-Debugging-Features sind mächtig für komplexe Interaktionen.

Die Wahl zwischen Tools hängt von Priorities ab. Jasmine/Karma ist batterie-included für Angular. Jest ist schneller und moderner. Cypress bietet besseres Debugging. Für die meisten Projekte bleibt der Standard die sichere Wahl – etabliert, gut dokumentiert, von der Angular CLI supportet.