Neuigkeiten von trion.
Immer gut informiert.

Debugging von Cypress-CI Netzwerkkommunikation

Angular & Cypress

Um Anwendungen sicher und verlässlich (weiter-)entwickeln zu können, ist es etablierte Praxis, Tests zu schreiben, die das Verhalten der Anwedung sicher nachstellen und überprüfen. Unterschieden wird dabei gerne zwischen feiner-granularen, eher technisch gelagerten Tests, wie den Unit-Tests, und Tests, die die Anwendung aus User-Sicht testen sollen. Das wird etwa mit Ende-zu-Ende oder auch E2E-Tests erreicht.

Jedoch kann es auch bei den Tests selbst zu spezifischen Problemen kommen, die ggf. nur innerhalb der Test-Umgebung oder sogar nur in einer speziellen Umgebung wie der CI-Pipeline auftauchen. So kann es beispielsweise zu Test-Fehlern kommen, wenn in der CI-Umgebung eine andere Authentifizierungsinfrastruktur, z.B. andere Client-Zertifikate, als in den restlichen Systemen verwendet wird.

CI-Prozesse laufen in der Regel auf separaten CI-Servern und in einer isolierten Umgebung, wie einer Container-Laufzeit, z.B. in Docker. Treten in dieser Umgebung nun Netzwerk-Fehler auf, gestaltet sich die Diagnose häufig aufwendig, da die Details der Netzwerk-Kommunikation nicht direkt einsehbar sind. Das wollen wir uns in diesem Artikel anhand einer Angular-Anwendung mit Cypress E2E-Tests verdeutlichen.
Zur besseren Untersuchung der Netzwerk-Kommunikation werden wir dann einzelne Requests innerhalb der E2E- bzw. Integrations-Tests in HAR-Dateien protokollieren. Dies wird automatisiert durch ein Cypress-Plugin realisiert. Die HAR-Dateien können dann auch in der CI-Umgebungen geschrieben und zur anschließenden Diagnose verwendet werden.

Zunächst setzen wir dazu ein neues Angular-Projekt (Projektname cypress-har) auf und fügen Cypress-E2E-Tests hinzu, wie in folgendem Listing dargestellt ist.

Bash-Kommandos zum Anlegen eines neuen Angular-Projektes mit Cypress-E2E-Tests
$ npm i -g @angular/cli@latest
$ ng new cypress-har --style=scss --ssr=false
$ cd cypress-har

$ ng add @cypress/schematic --e2e --skip-confirmation --component=false

Damit Cypress die HAR-Dateien aus den Tests heraus generieren kann, wird das Cypress-Plugin cypress-har-generator verwendet. Im folgenden Listing ist der Befehl zur Installtion des Paketes per npm gezeigt.

Bash-Kommando zum Installieren des Cypress-HAR-Plugin
$ npm i -D @neuralegion/cypress-har-generator

Damit der HAR-Generator funktioniert, muss er noch in der Cypress-Konfiguration berücksichtigt werden. Dafür muss in der Environment das Property “hars_folders” gesetzt werden, siehe dazu folgendes Listing.
Danach kann das Plugin aktiviert werden, dies geschieht innerhalb des setupNodeEvents-Hook, indem die install()-Funktion aufgerufen wird. Aufgrund von Cypress-Issue 5240 muss zusätzlich die Funktion ensureBrowserFlags() als Teil des “before:browser:launch”-Event-Handler aufgerufen werden.

Eintragen des Har-Generators in der cypress.config.ts
import { ensureBrowserFlags, install } from '@neuralegion/cypress-har-generator';
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    'baseUrl': 'http://localhost:4200',
    env: {
      "hars_folders": "cypress/hars"
    },
    setupNodeEvents(on) {
      install(on);
      on('before:browser:launch', (browser, launchOptions) => {
        ensureBrowserFlags(browser, launchOptions);
        return launchOptions;
      });
    }
  }
});

Der HAR-Generator muss in den Tests über eigene Cypress-Kommandos aufgerufen werden. Die Kommandos müssen dafür einmal importiert werden. Das wird in der Datei cypress/support/e2e.ts (in älteren Cypress-Versionen die cypress/support/index.ts), wie in folgendem Listing gezeigt, umgesetzt.

Importieren des HAR-Plugin in der cypress/support/e2e.ts
import '@neuralegion/cypress-har-generator/commands';

Für einen besseren TypeScript-Support (bspw. in Webstorm) sollten auch noch die HAR-Generator-Typinformationen innerhalb der cypress/tsconfig.ts zu den "types" hinzugefügt werden, wie auch folgendes Listing zeigt.

TypeScript-Support in die cypress/tsconfig.ts einbinden
{
  "extends": "../tsconfig.json",
  "include": ["**/*.ts"],
  "compilerOptions": {
    "sourceMap": false,
    "types": ["cypress", "@neuralegion/cypress-har-generator"]
  }
}

Um auch tatsächliche Netzwerk-Requests protokollieren zu können, müssen wir natürlich auch einen Netzwerk-Request zu unserer Anwendung hinzufügen. Dazu legen wir in unserem Projekt eine Datei mit Demo-Daten ab. Je nachdem, ob man das Projekt mit einer älteren oder einer etwas neueren Version von Angular-CLI aufgesetzt hat, muss die Datei im Ordner public/ (neu) oder src/assets/ (alt) abgelegt werden. Um die beiden Varianten zu vereinheitlichen, legen wir die Datei in diesem Beispiel unter dem Pfad public/assets/data ab.

Demo-Daten abgelegt in public/assets/data bzw. in älteren Angular-CLI-Projekten in src/assets/data
{
 "data": [ "test", "foo", "bar" ]
}

Die so abgelegten Daten können wir dann per Angular-HttpClient in der App abfragen. Dazu legen wir einen DemoService an, der eine GET-Abfrage gegen den Endpunkt 'assets/data' durchführt, siehe folgendes Listing.

Demo-Service mit HTTP-GET gegen assets/data
@Injectable({ providedIn: 'root' })
export class DemoService {

  constructor(private readonly http: HttpClient) { }

  getData(): Observable<{ data: string[] }> {
    return this.http.get<{ data: string[] }>('assets/data');
  }
}

Der DemoService wird dann per Angular-Dependency Injection in der DemoComponent zur Verfügung gestellt. Durch Aufruf der getData()-Methode werden dann die Daten ausgelesen und in der Komponente angezeigt. Zur Anzeige im Template wird die neue, seit Angular 18 als stabil markierte Control Flow Syntax verwendet, wie im folgendenden Listing auch zu sehen ist.

Demo-Component
@Component({
  selector: 'app-demo',
  standalone: true,
  template: `
    <p>Demo-Data:</p>
    @if (dataContainer|async; as container) {
      <div data-e2e="demo-data">
        @for (entry of container.data; track entry) {
          <div>{{ entry }}</div>
        }
      </div>
    }
  `,
  imports: [AsyncPipe]
})
export class DemoComponent {
  readonly dataContainer: Observable<{ data: string[] }>;

  constructor(demoService: DemoService) {
    this.dataContainer = demoService.getData();
  }
}

Für die so erzeugte Anwendung wird nun ein einfacher Cypress E2E-Test geschrieben.
Im Test wird die Seite per cy.visit() aufgerufen. Um auch sicherzustellen, dass die Demo-Daten erfolgreich geladen wurden, wird noch überprüft, ob der Text 'testfoobar' auf der Seite vorhanden ist.

Falls im Testablauf Fehler auftreten, werden diese von Cypress innerhalb von CI-Umgebungen typischerweise per Video und Screenshot protokolliert. Wenn aber beim Abruf der Demo-Daten ein Netzwerkfehler auftritt, ist der dahinter liegende Fehler nicht immer ganz einfach zu diagnostizieren, da Detail-Informationen zu Netzwerk-Requests typischerweise nicht Bestandteil der Videos und Screenshots ist.

Cypress Beispiel-Test
describe('My HAR-Demo Test', () => {

  it('loads the the app including all demo-data', () => {
    cy.visit('/');
    cy.get('[data-e2e="demo-data"]').should('include.text', 'testfoobar');
  });
});

Um in der CI-Umgebung nun detailliertere Informationen über die Netzwerk-Requests zu erhalten, setzen wir den HAR-Recorder ein. Der wird zunächst im before-Hook durch Aufruf von cy.recordHar() aktiviert. Nach Durchlauf der Tests können die erfolgten Requests dann in einer .har-Datei gespeichert werden. Das erledigt die cy.saveHar()-Funktion, hier beispielhaft im after-Test-Hook von Cypress aufgerufen. Wird das Property fileName weggelassen, so wird standardmäßig der Dateiname der Test-Datei als Grundlage für den Namen der HAR-Datei genommen.

Aktivieren des HAR-Recorder in E2E-Tests
describe('My HAR-Demo Test', () => {

  before(() => {
    // start recording
    cy.recordHar();
  });

  after(() => {
    // save the HAR file
    cy.saveHar({fileName: 'important-connections.har'});
  });

  it('loads the the app including all demo-data', () => {
    cy.visit('/');
    cy.get('[data-e2e="demo-data"]').should('include.text', 'testfoobar');
  });
});

Es ist zu beachten, dass das HAR-Plugin momentan nur mit Chrome-basierten Browsern funktioniert, also z.B. Chrome selbst oder Microsoft Edge. Damit der HAR-Generator arbeiten kann, muss zudem der Remote-Debugging-Port des Browsers aktiviert sein. Das kann durch den Kommandozeilen-Paramter --remote-debugging-port=9222 von Chrome erreicht werden. Damit der Parameter durch Cypress beim Aufruf des Browsers auch gesetzt wird, kann der Environment-Parameter ELECTRON_EXTRA_LAUNCH_ARGS beim Aufruf von ng e2e genutzt werden, siehe folgendes Listing.

Start des Tests mit aktiviertem Debugging-Port
$ ELECTRON_EXTRA_LAUNCH_ARGS=--remote-debugging-port=9222 ng e2e

Im Verlauf der Tests sollte im Cypress-Test-Log zu sehen sein, dass das HAR-Plugin aktiviert ist und die HAR-Datei geschrieben wurde. Die erwartete Ausageben ist im nebenstehenden Screenshot zu sehen.

Cypress Test Log

Die durch den Test generierte HAR-Datei kann dann verwendet werden, um Detail-Informationen zu den einzelnen Netzwerk-Requests auszulesen. Dazu gibt es spezielle HAR-Analyse-Tools. Als Beispiel wird hier der HAR-Analyzer aus der Google-Toolbox verwendet, siehe:

Dort können dann beispielsweise die Header von Request und Response, verwendete (Query-)Parameter, Timing-Informationen, Cookies und einige Informationen mehr zum Request dargestellt werden. Der Request für unsere Demo-Daten ist in folgendem Screenshot beispielhaft hervorgehoben.

Demo-Request per HAR-Analyzer dargesellt

Mit diesem Plugin und etwas Konfiguration ist es nun möglich auch bei schwer durch die Oberfläche zu diagnostizierenden Fehlern anhand der Netzwerkdaten von Tests in der CI-Pipeline eine Analyse durchzuführen




Zum Themen Spring Boot, Spring Security und Datenbanken bzw. SQL bieten wir sowohl Schulungen, Entwicklungsunterstützung als auch Beratung an:

Auch für Ihren individuellen Bedarf bieten wir gerne angepasste Workshops, Consulting und Schulungen an.
Sprechen Sie uns unverbindlich an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.