33 End-to-End Tests mit Cypress

End-to-End (E2E) Tests simulieren echte Benutzerinteraktionen und testen die gesamte Anwendung von der Benutzeroberfläche bis zur Datenebene. Cypress hat sich als führendes Framework für E2E-Testing in modernen Webanwendungen etabliert und bietet eine entwicklerfreundliche Alternative zu traditionellen Selenium-basierten Lösungen.

33.1 Was sind End-to-End Tests?

End-to-End Tests validieren komplette Benutzerszenarien und überprüfen, ob alle Komponenten einer Anwendung korrekt zusammenarbeiten. Sie testen die Anwendung aus der Perspektive des Endbenutzers und decken dabei Frontend, Backend, Datenbank und externe Services ab.

33.1.1 Einordnung in die Testpyramide

E2E Tests bilden die Spitze der Testpyramide. Sie sind langsamer und wartungsintensiver als Unit Tests oder Integration Tests, bieten aber die höchste Konfidenz, dass die Anwendung wie erwartet funktioniert.

// Typisches E2E-Szenario: Benutzer-Registrierung
// 1. Benutzer öffnet Registrierungsseite
// 2. Füllt Formular aus
// 3. Klickt auf "Registrieren"
// 4. System erstellt Account
// 5. Benutzer wird zur Dashboard-Seite weitergeleitet
// 6. Bestätigungs-E-Mail wird versendet

33.2 Cypress: Ein modernes E2E-Framework

Cypress unterscheidet sich fundamental von traditionellen E2E-Tools durch seine Architektur und Entwicklererfahrung.

33.2.1 Cypress-Architektur

Im Gegensatz zu Selenium läuft Cypress direkt im Browser und kommuniziert mit der Anwendung über eine native JavaScript-Umgebung. Dies ermöglicht direkten Zugriff auf DOM-Elemente, Netzwerk-Traffic und Browser-APIs.

33.2.2 Installation und Setup

npm install --save-dev cypress

Grundlegende package.json-Konfiguration:

{
  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run",
    "e2e": "cypress run --spec 'cypress/e2e/**/*.cy.js'"
  },
  "devDependencies": {
    "cypress": "^13.0.0"
  }
}

Cypress-Projektstruktur nach der Initialisierung:

cypress/
├── e2e/
│   └── spec.cy.js
├── fixtures/
│   └── example.json
├── support/
│   ├── commands.js
│   └── e2e.js
└── cypress.config.js

33.2.3 Grundkonfiguration

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    
    setupNodeEvents(on, config) {
      // Task-Handler für Backend-Operationen
      on('task', {
        seedDatabase() {
          // Datenbank für Tests vorbereiten
          return null;
        },
        
        clearDatabase() {
          // Testdaten nach Tests aufräumen
          return null;
        }
      });
    },
  },
});

33.3 Grundlegende Cypress-Syntax

33.3.1 Elementauswahl und Interaktion

// basic-interactions.cy.js
describe('Basic User Interactions', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('should log in user successfully', () => {
    // Eingabefelder finden und ausfüllen
    cy.get('[data-testid="email-input"]').type('user@example.com');
    cy.get('[data-testid="password-input"]').type('securepassword');
    
    // Submit-Button klicken
    cy.get('[data-testid="login-button"]').click();
    
    // Weiterleitung überprüfen
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome back!').should('be.visible');
  });

  it('should show error for invalid credentials', () => {
    cy.get('[data-testid="email-input"]').type('invalid@example.com');
    cy.get('[data-testid="password-input"]').type('wrongpassword');
    cy.get('[data-testid="login-button"]').click();
    
    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'Invalid credentials');
  });
});

33.3.2 Formular-Testing

// form-testing.cy.js
describe('Contact Form', () => {
  it('should submit contact form successfully', () => {
    cy.visit('/contact');
    
    // Formular ausfüllen
    cy.get('#name').type('John Doe');
    cy.get('#email').type('john@example.com');
    cy.get('#subject').select('General Inquiry');
    cy.get('#message').type('This is a test message from Cypress.');
    
    // Checkbox aktivieren
    cy.get('#newsletter').check();
    
    // Formular absenden
    cy.get('form').submit();
    
    // Erfolgsbestätigung überprüfen
    cy.get('.success-message')
      .should('be.visible')
      .and('contain', 'Thank you for your message');
    
    // Formular sollte zurückgesetzt sein
    cy.get('#name').should('have.value', '');
  });

  it('should validate required fields', () => {
    cy.visit('/contact');
    
    // Formular ohne Eingaben absenden
    cy.get('form').submit();
    
    // Validierungsfehler überprüfen
    cy.get('#name:invalid').should('exist');
    cy.get('#email:invalid').should('exist');
    
    // Fehlertooltips
    cy.get('#name').then(($input) => {
      expect($input[0].validationMessage).to.contain('Please fill out this field');
    });
  });
});
// navigation.cy.js
describe('Application Navigation', () => {
  it('should navigate through main sections', () => {
    cy.visit('/');
    
    // Hauptnavigation testen
    cy.get('[data-testid="nav-products"]').click();
    cy.url().should('include', '/products');
    cy.get('h1').should('contain', 'Our Products');
    
    cy.get('[data-testid="nav-about"]').click();
    cy.url().should('include', '/about');
    cy.get('h1').should('contain', 'About Us');
    
    // Breadcrumb-Navigation
    cy.get('.breadcrumb').should('contain', 'Home > About');
    
    // Browser-Navigation
    cy.go('back');
    cy.url().should('include', '/products');
  });

  it('should handle deep linking correctly', () => {
    // Direkter Aufruf einer Unterseite
    cy.visit('/products/laptop-123');
    
    cy.get('[data-testid="product-title"]').should('be.visible');
    cy.get('[data-testid="product-price"]').should('contain', '$');
    cy.get('[data-testid="add-to-cart"]').should('be.enabled');
  });
});

33.4 Erweiterte Cypress-Funktionen

33.4.1 API-Interception und Mocking

// api-testing.cy.js
describe('API Integration', () => {
  beforeEach(() => {
    // API-Aufrufe abfangen und mocken
    cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
    cy.intercept('POST', '/api/users', { 
      statusCode: 201, 
      body: { id: 123, name: 'New User' }
    }).as('createUser');
  });

  it('should load user list from API', () => {
    cy.visit('/users');
    
    // Warten auf API-Aufruf
    cy.wait('@getUsers');
    
    // UI-Aktualisierung überprüfen
    cy.get('[data-testid="user-list"]').should('be.visible');
    cy.get('[data-testid="user-item"]').should('have.length', 3);
  });

  it('should create new user via API', () => {
    cy.visit('/users/new');
    
    cy.get('#name').type('John Doe');
    cy.get('#email').type('john@example.com');
    cy.get('form').submit();
    
    cy.wait('@createUser').then((interception) => {
      expect(interception.request.body).to.deep.include({
        name: 'John Doe',
        email: 'john@example.com'
      });
    });
    
    cy.get('.success-notification').should('be.visible');
  });
});

33.4.2 Custom Commands

// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.session([email, password], () => {
    cy.visit('/login');
    cy.get('[data-testid="email-input"]').type(email);
    cy.get('[data-testid="password-input"]').type(password);
    cy.get('[data-testid="login-button"]').click();
    cy.url().should('include', '/dashboard');
  });
});

Cypress.Commands.add('addToCart', (productId, quantity = 1) => {
  cy.visit(`/products/${productId}`);
  
  if (quantity > 1) {
    cy.get('[data-testid="quantity-input"]').clear().type(quantity.toString());
  }
  
  cy.get('[data-testid="add-to-cart"]').click();
  cy.get('[data-testid="cart-count"]').should('contain', quantity);
});

Cypress.Commands.add('clearCart', () => {
  cy.visit('/cart');
  cy.get('body').then(($body) => {
    if ($body.find('[data-testid="clear-cart"]').length > 0) {
      cy.get('[data-testid="clear-cart"]').click();
      cy.get('[data-testid="confirm-clear"]').click();
    }
  });
});
// e-commerce-flow.cy.js
describe('E-Commerce User Journey', () => {
  beforeEach(() => {
    cy.clearCart();
    cy.login('customer@example.com', 'password123');
  });

  it('should complete purchase flow', () => {
    // Produkte zum Warenkorb hinzufügen
    cy.addToCart('laptop-123', 1);
    cy.addToCart('mouse-456', 2);
    
    // Zum Checkout
    cy.visit('/cart');
    cy.get('[data-testid="checkout-button"]').click();
    
    // Lieferadresse eingeben
    cy.get('#shipping-address').type('123 Main St, City, State 12345');
    cy.get('#shipping-method').select('standard');
    
    // Zahlungsinformationen
    cy.get('#card-number').type('4111111111111111');
    cy.get('#expiry').type('12/25');
    cy.get('#cvv').type('123');
    
    // Bestellung abschließen
    cy.get('[data-testid="place-order"]').click();
    
    // Bestätigung überprüfen
    cy.url().should('include', '/order-confirmation');
    cy.get('[data-testid="order-number"]').should('be.visible');
    cy.get('[data-testid="order-total"]').should('contain', '$');
  });
});

33.4.3 Datei-Upload und Download

// file-operations.cy.js
describe('File Operations', () => {
  it('should upload profile picture', () => {
    cy.visit('/profile');
    
    // Datei-Upload
    cy.get('input[type="file"]').selectFile('cypress/fixtures/profile-pic.jpg');
    cy.get('[data-testid="upload-button"]').click();
    
    // Upload-Fortschritt
    cy.get('.upload-progress').should('be.visible');
    cy.get('.upload-success', { timeout: 10000 }).should('be.visible');
    
    // Hochgeladenes Bild überprüfen
    cy.get('[data-testid="profile-image"]')
      .should('be.visible')
      .and('have.attr', 'src')
      .and('include', 'profile-pic');
  });

  it('should download report file', () => {
    cy.visit('/reports');
    
    // Download auslösen
    cy.get('[data-testid="download-report"]').click();
    
    // Datei-Download überprüfen
    cy.readFile('cypress/downloads/report.pdf').should('exist');
  });
});

33.5 E2E Test Patterns

33.5.1 Page Object Model

// cypress/support/pages/LoginPage.js
class LoginPage {
  visit() {
    cy.visit('/login');
    return this;
  }

  fillEmail(email) {
    cy.get('[data-testid="email-input"]').type(email);
    return this;
  }

  fillPassword(password) {
    cy.get('[data-testid="password-input"]').type(password);
    return this;
  }

  submit() {
    cy.get('[data-testid="login-button"]').click();
    return this;
  }

  shouldShowError(message) {
    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', message);
    return this;
  }

  shouldRedirectToDashboard() {
    cy.url().should('include', '/dashboard');
    return this;
  }
}

export default LoginPage;
// login-with-page-object.cy.js
import LoginPage from '../support/pages/LoginPage';

describe('Login with Page Object', () => {
  const loginPage = new LoginPage();

  it('should login successfully', () => {
    loginPage
      .visit()
      .fillEmail('user@example.com')
      .fillPassword('password123')
      .submit()
      .shouldRedirectToDashboard();
  });

  it('should show error for invalid credentials', () => {
    loginPage
      .visit()
      .fillEmail('invalid@example.com')
      .fillPassword('wrongpassword')
      .submit()
      .shouldShowError('Invalid credentials');
  });
});

33.5.2 Test Data Management

// cypress/fixtures/testData.js
export const users = {
  admin: {
    email: 'admin@example.com',
    password: 'admin123',
    role: 'administrator'
  },
  customer: {
    email: 'customer@example.com',
    password: 'customer123',
    role: 'customer'
  },
  guest: {
    email: 'guest@example.com',
    password: 'guest123',
    role: 'guest'
  }
};

export const products = {
  laptop: {
    id: 'laptop-123',
    name: 'Gaming Laptop',
    price: 1299.99,
    category: 'Electronics'
  },
  book: {
    id: 'book-456',
    name: 'JavaScript Guide',
    price: 29.99,
    category: 'Books'
  }
};
// data-driven-tests.cy.js
import { users, products } from '../fixtures/testData';

describe('Data-Driven E2E Tests', () => {
  Object.entries(users).forEach(([userType, userData]) => {
    it(`should allow ${userType} to browse products`, () => {
      cy.login(userData.email, userData.password);
      cy.visit('/products');
      
      cy.get('[data-testid="product-grid"]').should('be.visible');
      cy.get('[data-testid="product-item"]').should('have.length.greaterThan', 0);
      
      if (userData.role === 'admin') {
        cy.get('[data-testid="admin-panel"]').should('be.visible');
      }
    });
  });
});

33.6 Performance und Wartung

33.6.1 Test-Performance optimieren

// performance-optimized.cy.js
describe('Performance Optimized Tests', () => {
  before(() => {
    // Einmalige Setup-Operationen
    cy.task('seedDatabase');
  });

  beforeEach(() => {
    // Session-basierte Authentifizierung für schnellere Tests
    cy.session('user-session', () => {
      cy.login('user@example.com', 'password123');
    });
  });

  it('should load dashboard quickly', () => {
    cy.visit('/dashboard');
    
    // Performance-Metriken
    cy.window().its('performance').then((performance) => {
      const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
      expect(loadTime).to.be.lessThan(3000); // Max 3 Sekunden
    });
    
    // Kritische Elemente sollten schnell laden
    cy.get('[data-testid="main-content"]', { timeout: 2000 }).should('be.visible');
  });
});

33.6.2 Stabilität und Flaky Tests vermeiden

// stable-tests.cy.js
describe('Stable Test Practices', () => {
  it('should wait for dynamic content properly', () => {
    cy.visit('/dynamic-content');
    
    // Warten auf asynchrone Daten
    cy.get('[data-testid="loading-spinner"]').should('exist');
    cy.get('[data-testid="loading-spinner"]').should('not.exist');
    
    // Explizite Wartezeiten für Animationen
    cy.get('[data-testid="animated-element"]')
      .should('be.visible')
      .wait(500); // Animation abwarten
    
    // Retry-fähige Assertions
    cy.get('[data-testid="dynamic-list"]')
      .should('exist')
      .and('contain.text', 'Item 1');
  });

  it('should handle race conditions properly', () => {
    cy.intercept('GET', '/api/data', { delay: 1000, fixture: 'data.json' }).as('getData');
    
    cy.visit('/data-page');
    cy.wait('@getData'); // Warten auf API-Response
    
    cy.get('[data-testid="data-table"]').should('be.visible');
  });
});

33.7 Debugging und Troubleshooting

33.7.1 Debugging-Techniken

// debugging.cy.js
describe('Debugging Examples', () => {
  it('should provide debugging information', () => {
    cy.visit('/complex-page');
    
    // Debug-Ausgaben
    cy.get('[data-testid="user-info"]').then(($el) => {
      cy.log('User info element:', $el.text());
    });
    
    // Breakpoints für interaktives Debugging
    cy.get('[data-testid="problem-element"]').debug();
    
    // Screenshots für Fehleranalyse
    cy.screenshot('before-action');
    cy.get('[data-testid="action-button"]').click();
    cy.screenshot('after-action');
    
    // Pause für manuelle Inspektion (nur bei cy.open())
    // cy.pause();
  });
});

33.7.2 Error Handling

// error-handling.cy.js
describe('Error Handling', () => {
  it('should handle network errors gracefully', () => {
    // Netzwerkfehler simulieren
    cy.intercept('GET', '/api/critical-data', { forceNetworkError: true }).as('networkError');
    
    cy.visit('/page-with-api-dependency');
    
    // Fehlerbehandlung der Anwendung testen
    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'Unable to load data');
    
    cy.get('[data-testid="retry-button"]').should('be.visible');
  });

  it('should recover from errors', () => {
    cy.intercept('GET', '/api/data', { statusCode: 500 }).as('serverError');
    
    cy.visit('/data-page');
    cy.wait('@serverError');
    
    // Error-State überprüfen
    cy.get('[data-testid="error-state"]').should('be.visible');
    
    // Recovery-Mechanismus testen
    cy.intercept('GET', '/api/data', { fixture: 'data.json' }).as('successResponse');
    cy.get('[data-testid="retry-button"]').click();
    
    cy.wait('@successResponse');
    cy.get('[data-testid="data-content"]').should('be.visible');
  });
});

33.8 Cypress vs. andere E2E-Tools

33.8.1 Vergleich mit Selenium

Cypress Vorteile:

Selenium Vorteile:

// Cypress: Natürliche JavaScript-Syntax
cy.get('[data-testid="button"]').click();
cy.url().should('include', '/success');

// Selenium WebDriver: Mehr Boilerplate
// const button = await driver.findElement(By.css('[data-testid="button"]'));
// await button.click();
// const currentUrl = await driver.getCurrentUrl();
// assert(currentUrl.includes('/success'));

33.9 CI/CD Integration

33.9.1 GitHub Actions mit Cypress

# .github/workflows/e2e-tests.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Start application
        run: npm start &
        
      - name: Wait for server
        run: npx wait-on http://localhost:3000
        
      - name: Run Cypress tests
        uses: cypress-io/github-action@v5
        with:
          wait-on: 'http://localhost:3000'
          wait-on-timeout: 120
          
      - name: Upload screenshots
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots