Software Testing Grundlagen: Unit Tests, Integration Tests, E2E Tests, TDD & BDD
Dieser Beitrag ist eine umfassende Einführung in die Software Testing Grundlagen – inklusive Unit Tests, Integration Tests, E2E Tests, TDD und BDD mit praktischen Beispielen.
In a Nutshell
Software Testing ist ein systematischer Prozess zur Überprüfung der Qualität und Funktionalität von Software, um sicherzustellen, dass sie den Anforderungen entspricht und fehlerfrei funktioniert.
Kompakte Fachbeschreibung
Software Testing ist die Überprüfung von Software auf Korrektheit, Vollständigkeit und Qualität durch gezielte Tests und Validierungsmethoden.
Kernkomponenten:
Unit Tests
- Isolierte Tests: Einzelne Komponenten unabhängig testen
- Schnelle Ausführung: Millisekunden bis Sekunden
- Mocking: Abhängigkeiten simulieren
- Coverage: Code-Abdeckung messen
- Frameworks: JUnit, Jest, PyTest
Integration Tests
- Komponenten-Integration: Mehrere Module zusammen testen
- Datenbank-Tests: Persistenzschicht validieren
- API-Tests: Schnittstellen zwischen Services testen
- System-Integration: Gesamtsystem überprüfen
- Test-Umgebungen: Staging/Test-Systeme
End-to-End (E2E) Tests
- Benutzer-Szenarien: Komplette Workflows testen
- Browser-Automatisierung: UI-Interaktionen simulieren
- Cross-Browser: Verschiedene Browser testen
- Mobile Tests: iOS/Android Anwendungen
- Tools: Selenium, Cypress, Playwright
Test-Driven Development (TDD)
- Red-Green-Refactor: Zyklischer Entwicklungsprozess
- Test-First: Tests vor Implementierung schreiben
- Kleine Schritte: Inkrementelle Entwicklung
- Regression-Tests: Automatisierte Testsuite
- Design-Verbesserung: Bessere Software-Architektur
Behavior-Driven Development (BDD)
- Gherkin Syntax: Lesbare Testbeschreibungen
- User Stories: Geschäftsanforderungen formulieren
- Stakeholder-Kommunikation: Gemeinsames Verständnis
- Cucumber: BDD Framework Implementierung
- Living Documentation: Aktuelle Dokumentation
Prüfungsrelevante Stichpunkte
- Unit Tests: Isolierte Tests einzelner Funktionen/Klassen
- Integration Tests: Tests der Zusammenarbeit von Komponenten
- E2E Tests: Komplette Anwender-Workflows von Anfang bis Ende
- TDD: Test-Driven Development mit Red-Green-Refactor Zyklus
- BDD: Behavior-Driven Development mit Gherkin Syntax
- Test Pyramid: Mehr Unit Tests als Integration/E2E Tests
- Mocking: Simulation von Abhängigkeiten für Tests
- Code Coverage: Messung der Testabdeckung
- IHK-relevant: Software-Qualitätssicherung und Testing-Strategien
Kernkomponenten
- Test Strategy: Planung und Priorisierung von Tests
- Test Automation: Automatisierte Testausführung
- Test Frameworks: Tools und Bibliotheken für Tests
- Continuous Testing: Integration in CI/CD Pipelines
- Test Data Management: Testdaten generieren und verwalten
- Test Reporting: Ergebnisse analysieren und dokumentieren
- Performance Testing: Last- und Stresstests
- Security Testing: Sicherheitslücken aufdecken
Praxisbeispiele
1. Unit Tests mit JavaScript und Jest
// mathUtils.js - Zu testende Funktionen
export class MathUtils {
static add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
return a + b;
}
static subtract(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
return a - b;
}
static multiply(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
return a * b;
}
static divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}
static factorial(n) {
if (typeof n !== 'number' || n < 0 || !Number.isInteger(n)) {
throw new Error('Argument must be a non-negative integer');
}
if (n === 0 || n === 1) {
return 1;
}
return n * this.factorial(n - 1);
}
static fibonacci(n) {
if (typeof n !== 'number' || n < 0 || !Number.isInteger(n)) {
throw new Error('Argument must be a non-negative integer');
}
if (n === 0) {
return 0;
}
if (n === 1) {
return 1;
}
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
static isPrime(n) {
if (typeof n !== 'number' || n < 2 || !Number.isInteger(n)) {
return false;
}
if (n === 2) {
return true;
}
if (n % 2 === 0) {
return false;
}
for (let i = 3; i <= Math.sqrt(n); i += 2) {
if (n % i === 0) {
return false;
}
}
return true;
}
static gcd(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
a = Math.abs(a);
b = Math.abs(b);
while (b !== 0) {
const temp = b;
b = a % b;
a = temp;
}
return a;
}
static lcm(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
if (a === 0 || b === 0) {
return 0;
}
return Math.abs(a * b) / this.gcd(a, b);
}
}
// userService.js - Service mit Abhängigkeiten
export class UserService {
constructor(apiClient, cache) {
this.apiClient = apiClient;
this.cache = cache;
}
async getUser(id) {
if (!id) {
throw new Error('User ID is required');
}
// Check cache first
const cachedUser = this.cache.get(`user:${id}`);
if (cachedUser) {
return cachedUser;
}
// Fetch from API
const user = await this.apiClient.get(`/users/${id}`);
// Cache the result
this.cache.set(`user:${id}`, user, 300); // 5 minutes
return user;
}
async createUser(userData) {
if (!userData || !userData.email || !userData.name) {
throw new Error('Email and name are required');
}
const user = await this.apiClient.post('/users', userData);
// Cache the new user
this.cache.set(`user:${user.id}`, user, 300);
return user;
}
async updateUser(id, userData) {
if (!id || !userData) {
throw new Error('User ID and data are required');
}
const user = await this.apiClient.put(`/users/${id}`, userData);
// Update cache
this.cache.set(`user:${id}`, user, 300);
return user;
}
async deleteUser(id) {
if (!id) {
throw new Error('User ID is required');
}
await this.apiClient.delete(`/users/${id}`);
// Remove from cache
this.cache.delete(`user:${id}`);
return true;
}
async searchUsers(query) {
if (!query) {
throw new Error('Search query is required');
}
const cacheKey = `search:${query}`;
const cachedResults = this.cache.get(cacheKey);
if (cachedResults) {
return cachedResults;
}
const results = await this.apiClient.get('/users/search', { q: query });
this.cache.set(cacheKey, results, 600); // 10 minutes
return results;
}
}
// mathUtils.test.js - Unit Tests
import { MathUtils } from './mathUtils';
describe('MathUtils', () => {
describe('add', () => {
test('should add two positive numbers', () => {
expect(MathUtils.add(2, 3)).toBe(5);
expect(MathUtils.add(10, 20)).toBe(30);
});
test('should add negative numbers', () => {
expect(MathUtils.add(-2, -3)).toBe(-5);
expect(MathUtils.add(-10, 5)).toBe(-5);
});
test('should add zero', () => {
expect(MathUtils.add(0, 5)).toBe(5);
expect(MathUtils.add(5, 0)).toBe(5);
expect(MathUtils.add(0, 0)).toBe(0);
});
test('should handle decimal numbers', () => {
expect(MathUtils.add(1.5, 2.5)).toBe(4.0);
expect(MathUtils.add(-1.5, 2.5)).toBe(1.0);
});
test('should throw error for non-number arguments', () => {
expect(() => MathUtils.add('2', 3)).toThrow('Both arguments must be numbers');
expect(() => MathUtils.add(2, '3')).toThrow('Both arguments must be numbers');
expect(() => MathUtils.add(null, 3)).toThrow('Both arguments must be numbers');
expect(() => MathUtils.add(undefined, 3)).toThrow('Both arguments must be numbers');
});
});
describe('subtract', () => {
test('should subtract two positive numbers', () => {
expect(MathUtils.subtract(5, 3)).toBe(2);
expect(MathUtils.subtract(10, 20)).toBe(-10);
});
test('should subtract negative numbers', () => {
expect(MathUtils.subtract(-5, -3)).toBe(-2);
expect(MathUtils.subtract(-10, 5)).toBe(-15);
});
test('should throw error for non-number arguments', () => {
expect(() => MathUtils.subtract('5', 3)).toThrow('Both arguments must be numbers');
});
});
describe('divide', () => {
test('should divide two positive numbers', () => {
expect(MathUtils.divide(10, 2)).toBe(5);
expect(MathUtils.divide(7, 2)).toBe(3.5);
});
test('should handle division by zero', () => {
expect(() => MathUtils.divide(10, 0)).toThrow('Division by zero is not allowed');
});
test('should handle negative numbers', () => {
expect(MathUtils.divide(-10, 2)).toBe(-5);
expect(MathUtils.divide(10, -2)).toBe(-5);
expect(MathUtils.divide(-10, -2)).toBe(5);
});
});
describe('factorial', () => {
test('should calculate factorial of positive numbers', () => {
expect(MathUtils.factorial(0)).toBe(1);
expect(MathUtils.factorial(1)).toBe(1);
expect(MathUtils.factorial(5)).toBe(120);
expect(MathUtils.factorial(10)).toBe(3628800);
});
test('should throw error for negative numbers', () => {
expect(() => MathUtils.factorial(-1)).toThrow('Argument must be a non-negative integer');
});
test('should throw error for non-integers', () => {
expect(() => MathUtils.factorial(2.5)).toThrow('Argument must be a non-negative integer');
});
});
describe('fibonacci', () => {
test('should calculate fibonacci numbers', () => {
expect(MathUtils.fibonacci(0)).toBe(0);
expect(MathUtils.fibonacci(1)).toBe(1);
expect(MathUtils.fibonacci(5)).toBe(5);
expect(MathUtils.fibonacci(10)).toBe(55);
});
test('should throw error for negative numbers', () => {
expect(() => MathUtils.fibonacci(-1)).toThrow('Argument must be a non-negative integer');
});
});
describe('isPrime', () => {
test('should identify prime numbers correctly', () => {
expect(MathUtils.isPrime(2)).toBe(true);
expect(MathUtils.isPrime(3)).toBe(true);
expect(MathUtils.isPrime(5)).toBe(true);
expect(MathUtils.isPrime(7)).toBe(true);
expect(MathUtils.isPrime(11)).toBe(true);
});
test('should identify non-prime numbers correctly', () => {
expect(MathUtils.isPrime(1)).toBe(false);
expect(MathUtils.isPrime(4)).toBe(false);
expect(MathUtils.isPrime(6)).toBe(false);
expect(MathUtils.isPrime(8)).toBe(false);
expect(MathUtils.isPrime(9)).toBe(false);
expect(MathUtils.isPrime(10)).toBe(false);
});
});
describe('gcd', () => {
test('should calculate greatest common divisor', () => {
expect(MathUtils.gcd(48, 18)).toBe(6);
expect(MathUtils.gcd(54, 24)).toBe(6);
expect(MathUtils.gcd(17, 23)).toBe(1);
expect(MathUtils.gcd(0, 5)).toBe(5);
expect(MathUtils.gcd(5, 0)).toBe(5);
});
test('should handle negative numbers', () => {
expect(MathUtils.gcd(-48, 18)).toBe(6);
expect(MathUtils.gcd(48, -18)).toBe(6);
expect(MathUtils.gcd(-48, -18)).toBe(6);
});
});
describe('lcm', () => {
test('should calculate least common multiple', () => {
expect(MathUtils.lcm(4, 6)).toBe(12);
expect(MathUtils.lcm(21, 6)).toBe(42);
expect(MathUtils.lcm(3, 5)).toBe(15);
});
test('should handle zero', () => {
expect(MathUtils.lcm(0, 5)).toBe(0);
expect(MathUtils.lcm(5, 0)).toBe(0);
expect(MathUtils.lcm(0, 0)).toBe(0);
});
});
});
// userService.test.js - Unit Tests mit Mocking
import { UserService } from './userService';
// Mock dependencies
const mockApiClient = {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn()
};
const mockCache = {
get: jest.fn(),
set: jest.fn(),
delete: jest.fn()
};
describe('UserService', () => {
let userService;
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks();
userService = new UserService(mockApiClient, mockCache);
});
describe('getUser', () => {
test('should return user from cache if available', async () => {
const cachedUser = { id: 1, name: 'John Doe', email: 'john@example.com' };
mockCache.get.mockReturnValue(cachedUser);
const result = await userService.getUser(1);
expect(result).toBe(cachedUser);
expect(mockCache.get).toHaveBeenCalledWith('user:1');
expect(mockApiClient.get).not.toHaveBeenCalled();
});
test('should fetch user from API if not in cache', async () => {
const user = { id: 1, name: 'John Doe', email: 'john@example.com' };
mockCache.get.mockReturnValue(null);
mockApiClient.get.mockResolvedValue(user);
const result = await userService.getUser(1);
expect(result).toBe(user);
expect(mockCache.get).toHaveBeenCalledWith('user:1');
expect(mockApiClient.get).toHaveBeenCalledWith('/users/1');
expect(mockCache.set).toHaveBeenCalledWith('user:1', user, 300);
});
test('should throw error if no user ID provided', async () => {
await expect(userService.getUser()).rejects.toThrow('User ID is required');
});
test('should handle API errors', async () => {
mockCache.get.mockReturnValue(null);
mockApiClient.get.mockRejectedValue(new Error('API Error'));
await expect(userService.getUser(1)).rejects.toThrow('API Error');
});
});
describe('createUser', () => {
test('should create user successfully', async () => {
const userData = { name: 'John Doe', email: 'john@example.com' };
const createdUser = { id: 1, ...userData };
mockApiClient.post.mockResolvedValue(createdUser);
const result = await userService.createUser(userData);
expect(result).toBe(createdUser);
expect(mockApiClient.post).toHaveBeenCalledWith('/users', userData);
expect(mockCache.set).toHaveBeenCalledWith('user:1', createdUser, 300);
});
test('should throw error if email is missing', async () => {
const userData = { name: 'John Doe' };
await expect(userService.createUser(userData)).rejects.toThrow('Email and name are required');
});
test('should throw error if name is missing', async () => {
const userData = { email: 'john@example.com' };
await expect(userService.createUser(userData)).rejects.toThrow('Email and name are required');
});
});
describe('updateUser', () => {
test('should update user successfully', async () => {
const userData = { name: 'Jane Doe' };
const updatedUser = { id: 1, name: 'Jane Doe', email: 'john@example.com' };
mockApiClient.put.mockResolvedValue(updatedUser);
const result = await userService.updateUser(1, userData);
expect(result).toBe(updatedUser);
expect(mockApiClient.put).toHaveBeenCalledWith('/users/1', userData);
expect(mockCache.set).toHaveBeenCalledWith('user:1', updatedUser, 300);
});
test('should throw error if no user ID provided', async () => {
await expect(userService.updateUser()).rejects.toThrow('User ID and data are required');
});
});
describe('searchUsers', () => {
test('should return cached search results if available', async () => {
const cachedResults = [{ id: 1, name: 'John Doe' }];
mockCache.get.mockReturnValue(cachedResults);
const result = await userService.searchUsers('John');
expect(result).toBe(cachedResults);
expect(mockCache.get).toHaveBeenCalledWith('search:John');
expect(mockApiClient.get).not.toHaveBeenCalled();
});
test('should search users via API if not in cache', async () => {
const results = [{ id: 1, name: 'John Doe' }];
mockCache.get.mockReturnValue(null);
mockApiClient.get.mockResolvedValue(results);
const result = await userService.searchUsers('John');
expect(result).toBe(results);
expect(mockCache.get).toHaveBeenCalledWith('search:John');
expect(mockApiClient.get).toHaveBeenCalledWith('/users/search', { q: 'John' });
expect(mockCache.set).toHaveBeenCalledWith('search:John', results, 600);
});
});
});
// Test Configuration
// jest.config.js
module.exports = {
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/**/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
testMatch: [
'**/__tests__/**/*.js',
'**/?(*.)+(spec|test).js'
]
};
// tests/setup.js
// Global test setup
beforeAll(() => {
console.log('Starting test suite');
});
afterAll(() => {
console.log('Test suite completed');
});
beforeEach(() => {
// Reset console mocks
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
// Restore console mocks
console.log.mockRestore();
console.error.mockRestore();
});
2. Integration Tests mit Python und PyTest
# tests/integration/test_user_integration.py
import pytest
import asyncio
from httpx import AsyncClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db, Base
from app.models import User, Post, Comment
from app.core.config import settings
# Test database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Override database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
@pytest.fixture(scope="session")
def event_loop():
"""Create an instance of the default event loop for the test session."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="function")
def db_session():
"""Create a fresh database session for each test."""
Base.metadata.create_all(bind=engine)
session = TestingSessionLocal()
try:
yield session
finally:
session.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db_session):
"""Create a test client."""
with TestClient(app) as test_client:
yield test_client
@pytest.fixture(scope="function")
async def async_client(db_session):
"""Create an async test client."""
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.fixture
def sample_user(db_session):
"""Create a sample user for testing."""
user = User(
username="testuser",
email="test@example.com",
hashed_password="hashed_password",
is_active=True
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
return user
@pytest.fixture
def sample_post(db_session, sample_user):
"""Create a sample post for testing."""
post = Post(
title="Test Post",
content="This is a test post content.",
author_id=sample_user.id,
is_published=True
)
db_session.add(post)
db_session.commit()
db_session.refresh(post)
return post
@pytest.fixture
def sample_comment(db_session, sample_user, sample_post):
"""Create a sample comment for testing."""
comment = Comment(
content="This is a test comment.",
author_id=sample_user.id,
post_id=sample_post.id
)
db_session.add(comment)
db_session.commit()
db_session.refresh(comment)
return comment
class TestUserIntegration:
"""Integration tests for user endpoints."""
def test_create_user_success(self, client):
"""Test successful user creation."""
user_data = {
"username": "newuser",
"email": "newuser@example.com",
"password": "securepassword123"
}
response = client.post("/api/v1/users/", json=user_data)
assert response.status_code == 201
data = response.json()
assert data["username"] == user_data["username"]
assert data["email"] == user_data["email"]
assert "id" in data
assert "hashed_password" not in data # Password should not be returned
def test_create_user_duplicate_email(self, client, sample_user):
"""Test user creation with duplicate email."""
user_data = {
"username": "anotheruser",
"email": sample_user.email, # Duplicate email
"password": "securepassword123"
}
response = client.post("/api/v1/users/", json=user_data)
assert response.status_code == 400
assert "already registered" in response.json()["detail"].lower()
def test_get_user_success(self, client, sample_user):
"""Test successful user retrieval."""
response = client.get(f"/api/v1/users/{sample_user.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_user.id
assert data["username"] == sample_user.username
assert data["email"] == sample_user.email
def test_get_user_not_found(self, client):
"""Test user retrieval with non-existent ID."""
response = client.get("/api/v1/users/99999")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_update_user_success(self, client, sample_user):
"""Test successful user update."""
update_data = {
"username": "updateduser",
"email": "updated@example.com"
}
response = client.put(f"/api/v1/users/{sample_user.id}", json=update_data)
assert response.status_code == 200
data = response.json()
assert data["username"] == update_data["username"]
assert data["email"] == update_data["email"]
def test_delete_user_success(self, client, sample_user):
"""Test successful user deletion."""
response = client.delete(f"/api/v1/users/{sample_user.id}")
assert response.status_code == 204
# Verify user is deleted
response = client.get(f"/api/v1/users/{sample_user.id}")
assert response.status_code == 404
class TestPostIntegration:
"""Integration tests for post endpoints."""
def test_create_post_success(self, client, sample_user):
"""Test successful post creation."""
post_data = {
"title": "New Test Post",
"content": "This is a new test post content.",
"is_published": True
}
# Authenticate as sample user
client.headers.update({"Authorization": f"Bearer {sample_user.id}"})
response = client.post("/api/v1/posts/", json=post_data)
assert response.status_code == 201
data = response.json()
assert data["title"] == post_data["title"]
assert data["content"] == post_data["content"]
assert data["author_id"] == sample_user.id
def test_get_post_success(self, client, sample_post):
"""Test successful post retrieval."""
response = client.get(f"/api/v1/posts/{sample_post.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_post.id
assert data["title"] == sample_post.title
assert data["author"]["id"] == sample_post.author_id
def test_get_posts_with_pagination(self, client, db_session):
"""Test post listing with pagination."""
# Create multiple posts
user = db_session.query(User).first()
for i in range(25):
post = Post(
title=f"Post {i}",
content=f"Content for post {i}",
author_id=user.id,
is_published=True
)
db_session.add(post)
db_session.commit()
response = client.get("/api/v1/posts/?skip=0&limit=10")
assert response.status_code == 200
data = response.json()
assert len(data) == 10
assert "total" in response.headers
def test_update_post_success(self, client, sample_post, sample_user):
"""Test successful post update."""
update_data = {
"title": "Updated Post Title",
"content": "Updated post content."
}
# Authenticate as post author
client.headers.update({"Authorization": f"Bearer {sample_user.id}"})
response = client.put(f"/api/v1/posts/{sample_post.id}", json=update_data)
assert response.status_code == 200
data = response.json()
assert data["title"] == update_data["title"]
assert data["content"] == update_data["content"]
def test_delete_post_success(self, client, sample_post, sample_user):
"""Test successful post deletion."""
# Authenticate as post author
client.headers.update({"Authorization": f"Bearer {sample_user.id}"})
response = client.delete(f"/api/v1/posts/{sample_post.id}")
assert response.status_code == 204
# Verify post is deleted
response = client.get(f"/api/v1/posts/{sample_post.id}")
assert response.status_code == 404
class TestCommentIntegration:
"""Integration tests for comment endpoints."""
def test_create_comment_success(self, client, sample_post, sample_user):
"""Test successful comment creation."""
comment_data = {
"content": "This is a test comment.",
"post_id": sample_post.id
}
# Authenticate as sample user
client.headers.update({"Authorization": f"Bearer {sample_user.id}"})
response = client.post("/api/v1/comments/", json=comment_data)
assert response.status_code == 201
data = response.json()
assert data["content"] == comment_data["content"]
assert data["post_id"] == sample_post.id
assert data["author_id"] == sample_user.id
def test_get_comments_for_post(self, client, sample_post, sample_comment):
"""Test retrieving comments for a post."""
response = client.get(f"/api/v1/posts/{sample_post.id}/comments")
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
assert any(comment["id"] == sample_comment.id for comment in data)
class TestAPIIntegration:
"""Integration tests for API endpoints."""
def test_user_post_relationship(self, client, sample_user, sample_post):
"""Test user-post relationship."""
response = client.get(f"/api/v1/users/{sample_user.id}/posts")
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
assert any(post["id"] == sample_post.id for post in data)
def test_post_comment_relationship(self, client, sample_post, sample_comment):
"""Test post-comment relationship."""
response = client.get(f"/api/v1/posts/{sample_post.id}/comments")
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
assert any(comment["id"] == sample_comment.id for comment in data)
def test_search_functionality(self, client, sample_post):
"""Test search functionality."""
response = client.get(f"/api/v1/search?q={sample_post.title}")
assert response.status_code == 200
data = response.json()
assert "posts" in data
assert "users" in data
assert len(data["posts"]) >= 1
class TestDatabaseIntegration:
"""Integration tests for database operations."""
def test_transaction_rollback_on_error(self, client, sample_user):
"""Test transaction rollback on error."""
# This test would verify that database transactions are properly
# rolled back when an error occurs during a complex operation
post_data = {
"title": "Valid Post",
"content": "Valid content",
"is_published": True
}
# Simulate a database error during post creation
with pytest.raises(Exception):
# This would be implemented with actual database error simulation
pass
# Verify that no partial data was saved
response = client.get("/api/v1/posts/")
data = response.json()
assert not any(post["title"] == "Valid Post" for post in data)
def test_concurrent_requests(self, async_client, sample_user):
"""Test handling of concurrent requests."""
import asyncio
async def create_post(title):
post_data = {
"title": title,
"content": f"Content for {title}",
"is_published": True
}
response = await async_client.post(
"/api/v1/posts/",
json=post_data,
headers={"Authorization": f"Bearer {sample_user.id}"}
)
return response
# Create multiple concurrent requests
tasks = [
create_post(f"Concurrent Post {i}")
for i in range(10)
]
responses = await asyncio.gather(*tasks)
# Verify all requests succeeded
for response in responses:
assert response.status_code == 201
# Verify all posts were created
response = await async_client.get("/api/v1/posts/")
data = response.json()
concurrent_posts = [
post for post in data
if post["title"].startswith("Concurrent Post")
]
assert len(concurrent_posts) == 10
class TestAuthenticationIntegration:
"""Integration tests for authentication."""
def test_login_success(self, client, sample_user):
"""Test successful login."""
login_data = {
"username": sample_user.username,
"password": "testpassword" # This would be the actual password
}
response = client.post("/api/v1/auth/login", json=login_data)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "token_type" in data
assert data["token_type"] == "bearer"
def test_protected_endpoint_without_token(self, client):
"""Test accessing protected endpoint without token."""
response = client.post("/api/v1/posts/", json={
"title": "Test Post",
"content": "Test content"
})
assert response.status_code == 401
assert "not authenticated" in response.json()["detail"].lower()
def test_protected_endpoint_with_invalid_token(self, client):
"""Test accessing protected endpoint with invalid token."""
client.headers.update({"Authorization": "Bearer invalid_token"})
response = client.post("/api/v1/posts/", json={
"title": "Test Post",
"content": "Test content"
})
assert response.status_code == 401
# Performance Tests
class TestPerformanceIntegration:
"""Integration tests for performance."""
def test_bulk_operations_performance(self, client, sample_user):
"""Test performance of bulk operations."""
import time
client.headers.update({"Authorization": f"Bearer {sample_user.id}"})
# Measure time for creating multiple posts
start_time = time.time()
for i in range(100):
post_data = {
"title": f"Bulk Post {i}",
"content": f"Bulk content {i}",
"is_published": True
}
response = client.post("/api/v1/posts/", json=post_data)
assert response.status_code == 201
end_time = time.time()
duration = end_time - start_time
# Should complete within reasonable time (adjust threshold as needed)
assert duration < 10.0, f"Bulk operations took too long: {duration}s"
def test_large_payload_handling(self, client, sample_user):
"""Test handling of large payloads."""
client.headers.update({"Authorization": f"Bearer {sample_user.id}"})
# Create post with very large content
large_content = "A" * 1000000 # 1MB of content
post_data = {
"title": "Large Content Post",
"content": large_content,
"is_published": True
}
response = client.post("/api/v1/posts/", json=post_data)
# Should handle large payload (or return appropriate error)
assert response.status_code in [201, 413, 422]
# Error Handling Tests
class TestErrorHandlingIntegration:
"""Integration tests for error handling."""
def test_malformed_json_request(self, client):
"""Test handling of malformed JSON."""
response = client.post(
"/api/v1/users/",
data="invalid json",
headers={"Content-Type": "application/json"}
)
assert response.status_code == 422
def test_invalid_endpoint(self, client):
"""Test handling of invalid endpoints."""
response = client.get("/api/v1/invalid/endpoint")
assert response.status_code == 404
def test_method_not_allowed(self, client):
"""Test handling of disallowed HTTP methods."""
response = client.patch("/api/v1/users/")
assert response.status_code == 405
# Configuration Tests
class TestConfigurationIntegration:
"""Integration tests for configuration."""
def test_cors_headers(self, client):
"""Test CORS headers are properly set."""
response = client.options("/api/v1/users/")
assert "access-control-allow-origin" in response.headers
def test_rate_limiting(self, client):
"""Test rate limiting is working."""
# Make multiple rapid requests
responses = []
for i in range(100):
response = client.get("/api/v1/users/")
responses.append(response)
if response.status_code == 429:
break
# Should eventually hit rate limit
rate_limited = any(r.status_code == 429 for r in responses)
assert rate_limited, "Rate limiting not working"
# Test utilities
@pytest.fixture
def auth_headers(sample_user):
"""Create authentication headers for testing."""
return {"Authorization": f"Bearer {sample_user.id}"}
@pytest.fixture
def create_multiple_posts(db_session, sample_user):
"""Create multiple posts for testing."""
posts = []
for i in range(10):
post = Post(
title=f"Test Post {i}",
content=f"Content for test post {i}",
author_id=sample_user.id,
is_published=True
)
db_session.add(post)
posts.append(post)
db_session.commit()
return posts
# Custom assertions
def assert_valid_user_response(response_data):
"""Assert that user response data is valid."""
required_fields = ["id", "username", "email", "is_active"]
for field in required_fields:
assert field in response_data, f"Missing required field: {field}"
assert "hashed_password" not in response_data, "Password should not be returned"
def assert_valid_post_response(response_data):
"""Assert that post response data is valid."""
required_fields = ["id", "title", "content", "author_id", "created_at"]
for field in required_fields:
assert field in response_data, f"Missing required field: {field}"
assert "author" in response_data, "Author information should be included"
3. End-to-End Tests mit Cypress
// cypress/e2e/user-journey.cy.js
describe('User Journey E2E Tests', () => {
beforeEach(() => {
// Clear cookies and localStorage before each test
cy.clearCookies();
cy.clearLocalStorage();
// Visit the application
cy.visit('/');
});
describe('Registration and Login Flow', () => {
it('should allow user to register and login successfully', () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'SecurePassword123!'
};
// Navigate to registration page
cy.get('[data-testid="nav-register"]').click();
cy.url().should('include', '/register');
// Fill registration form
cy.get('[data-testid="username-input"]').type(userData.username);
cy.get('[data-testid="email-input"]').type(userData.email);
cy.get('[data-testid="password-input"]').type(userData.password);
cy.get('[data-testid="confirm-password-input"]').type(userData.password);
// Submit registration
cy.get('[data-testid="register-button"]').click();
// Should redirect to login page
cy.url().should('include', '/login');
// Should show success message
cy.get('[data-testid="success-message"]')
.should('be.visible')
.and('contain', 'Registration successful');
// Login with new credentials
cy.get('[data-testid="username-input"]').type(userData.username);
cy.get('[data-testid="password-input"]').type(userData.password);
cy.get('[data-testid="login-button"]').click();
// Should redirect to dashboard
cy.url().should('include', '/dashboard');
// Should show user information
cy.get('[data-testid="user-menu"]').should('contain', userData.username);
});
it('should show validation errors for invalid registration', () => {
// Navigate to registration page
cy.get('[data-testid="nav-register"]').click();
// Submit empty form
cy.get('[data-testid="register-button"]').click();
// Should show validation errors
cy.get('[data-testid="username-error"]').should('be.visible');
cy.get('[data-testid="email-error"]').should('be.visible');
cy.get('[data-testid="password-error"]').should('be.visible');
// Try with invalid email
cy.get('[data-testid="email-input"]').type('invalid-email');
cy.get('[data-testid="register-button"]').click();
cy.get('[data-testid="email-error"]')
.should('be.visible')
.and('contain', 'valid email');
// Try with weak password
cy.get('[data-testid="username-input"]').type('testuser');
cy.get('[data-testid="password-input"]').type('123');
cy.get('[data-testid="register-button"]').click();
cy.get('[data-testid="password-error"]')
.should('be.visible')
.and('contain', 'at least 8 characters');
});
});
describe('Post Creation and Management', () => {
beforeEach(() => {
// Login before each test
cy.login('testuser', 'SecurePassword123!');
});
it('should create, edit, and delete a post successfully', () => {
const postData = {
title: 'My Test Post',
content: 'This is the content of my test post. It should be long enough to be valid.',
tags: ['testing', 'cypress', 'e2e']
};
// Navigate to create post page
cy.get('[data-testid="nav-create-post"]').click();
cy.url().should('include', '/posts/create');
// Fill post form
cy.get('[data-testid="title-input"]').type(postData.title);
cy.get('[data-testid="content-textarea"]').type(postData.content);
// Add tags
postData.tags.forEach(tag => {
cy.get('[data-testid="tag-input"]').type(`${tag}{enter}`);
});
// Submit form
cy.get('[data-testid="create-post-button"]').click();
// Should redirect to post detail page
cy.url().should('match', /\/posts\/\d+/);
// Verify post content
cy.get('[data-testid="post-title"]').should('contain', postData.title);
cy.get('[data-testid="post-content"]').should('contain', postData.content);
// Verify tags
postData.tags.forEach(tag => {
cy.get('[data-testid="post-tags"]').should('contain', tag);
});
// Edit the post
cy.get('[data-testid="edit-post-button"]').click();
cy.url().should('match', /\/posts\/\d+\/edit/);
const updatedTitle = 'Updated Test Post';
cy.get('[data-testid="title-input"]').clear().type(updatedTitle);
cy.get('[data-testid="update-post-button"]').click();
// Verify updated content
cy.get('[data-testid="post-title"]').should('contain', updatedTitle);
// Add a comment
const commentText = 'This is a test comment';
cy.get('[data-testid="comment-input"]').type(commentText);
cy.get('[data-testid="add-comment-button"]').click();
// Verify comment appears
cy.get('[data-testid="comments-section"]')
.should('contain', commentText);
// Like the post
cy.get('[data-testid="like-button"]').click();
cy.get('[data-testid="like-button"]')
.should('have.class', 'liked')
.and('contain', '1');
// Unlike the post
cy.get('[data-testid="like-button"]').click();
cy.get('[data-testid="like-button"]')
.should('not.have.class', 'liked')
.and('contain', '0');
// Delete the post
cy.get('[data-testid="delete-post-button"]').click();
cy.get('[data-testid="confirm-delete"]').click();
// Should redirect to posts list
cy.url().should('include', '/posts');
// Verify post is no longer in list
cy.get('[data-testid="posts-list"]').should('not.contain', postData.title);
});
it('should search and filter posts', () => {
// Create multiple posts with different content
const posts = [
{ title: 'JavaScript Tutorial', content: 'Learn JavaScript programming', tags: ['javascript', 'tutorial'] },
{ title: 'Python Guide', content: 'Python programming guide', tags: ['python', 'guide'] },
{ title: 'React Patterns', content: 'Common React patterns', tags: ['react', 'patterns'] }
];
posts.forEach(post => {
cy.createPost(post.title, post.content, post.tags);
});
// Navigate to posts list
cy.get('[data-testid="nav-posts"]').click();
cy.url().should('include', '/posts');
// Test search functionality
cy.get('[data-testid="search-input"]').type('JavaScript');
cy.get('[data-testid="search-button"]').click();
// Should only show JavaScript post
cy.get('[data-testid="posts-list"]')
.should('contain', 'JavaScript Tutorial')
.and('not.contain', 'Python Guide')
.and('not.contain', 'React Patterns');
// Clear search
cy.get('[data-testid="clear-search"]').click();
// Test tag filtering
cy.get('[data-testid="tag-filter"]').select('javascript');
cy.get('[data-testid="posts-list"]')
.should('contain', 'JavaScript Tutorial')
.and('not.contain', 'Python Guide');
// Test sorting
cy.get('[data-testid="sort-select"]').select('title');
// Posts should be sorted alphabetically
cy.get('[data-testid="posts-list"] [data-testid="post-title"]')
.then($titles => {
const titles = $titles.map((i, el) => Cypress.$(el).text()).get();
const sortedTitles = [...titles].sort();
expect(titles).to.deep.equal(sortedTitles);
});
});
});
describe('User Profile Management', () => {
beforeEach(() => {
cy.login('testuser', 'SecurePassword123!');
});
it('should update user profile successfully', () => {
const profileData = {
firstName: 'John',
lastName: 'Doe',
bio: 'Software developer passionate about testing',
location: 'San Francisco, CA'
};
// Navigate to profile page
cy.get('[data-testid="nav-profile"]').click();
cy.url().should('include', '/profile');
// Edit profile
cy.get('[data-testid="edit-profile-button"]').click();
// Fill profile form
cy.get('[data-testid="first-name-input"]').type(profileData.firstName);
cy.get('[data-testid="last-name-input"]').type(profileData.lastName);
cy.get('[data-testid="bio-textarea"]').type(profileData.bio);
cy.get('[data-testid="location-input"]').type(profileData.location);
// Upload profile picture
cy.get('[data-testid="avatar-upload"]').attachFile('fixtures/avatar.jpg');
// Save profile
cy.get('[data-testid="save-profile-button"]').click();
// Verify profile updates
cy.get('[data-testid="profile-display"]').should('contain', profileData.firstName);
cy.get('[data-testid="profile-display"]').should('contain', profileData.lastName);
cy.get('[data-testid="profile-display"]').should('contain', profileData.bio);
cy.get('[data-testid="profile-display"]').should('contain', profileData.location);
// Verify avatar is uploaded
cy.get('[data-testid="profile-avatar"]')
.should('have.attr', 'src')
.and('match', /avatar/);
});
it('should change password successfully', () => {
const newPassword = 'NewSecurePassword456!';
// Navigate to settings
cy.get('[data-testid="nav-settings"]').click();
cy.url().should('include', '/settings');
// Change password
cy.get('[data-testid="current-password-input"]').type('SecurePassword123!');
cy.get('[data-testid="new-password-input"]').type(newPassword);
cy.get('[data-testid="confirm-new-password-input"]').type(newPassword);
cy.get('[data-testid="change-password-button"]').click();
// Should show success message
cy.get('[data-testid="success-message"]')
.should('be.visible')
.and('contain', 'Password changed successfully');
// Logout and login with new password
cy.get('[data-testid="nav-logout"]').click();
cy.url().should('include', '/login');
cy.get('[data-testid="username-input"]').type('testuser');
cy.get('[data-testid="password-input"]').type(newPassword);
cy.get('[data-testid="login-button"]').click();
// Should login successfully
cy.url().should('include', '/dashboard');
});
});
describe('Responsive Design', () => {
it('should work on mobile devices', () => {
// Set mobile viewport
cy.viewport('iphone-x');
// Test navigation menu
cy.get('[data-testid="mobile-menu-toggle"]').should('be.visible').click();
cy.get('[data-testid="mobile-menu"]').should('be.visible');
// Test responsive layout
cy.visit('/posts');
cy.get('[data-testid="posts-grid"]').should('be.visible');
// Posts should be in single column on mobile
cy.get('[data-testid="posts-grid"]')
.should('have.css', 'grid-template-columns')
.and('match', /1fr/);
// Test mobile forms
cy.get('[data-testid="nav-create-post"]').click();
cy.get('[data-testid="mobile-form"]').should('be.visible');
// Form should be full width on mobile
cy.get('[data-testid="create-post-form"]')
.should('have.css', 'width')
.and('match', /100%/);
});
it('should work on tablet devices', () => {
// Set tablet viewport
cy.viewport('ipad-2');
// Test tablet layout
cy.visit('/posts');
cy.get('[data-testid="posts-grid"]').should('be.visible');
// Posts should be in two columns on tablet
cy.get('[data-testid="posts-grid"]')
.should('have.css', 'grid-template-columns')
.and('match', /1fr.*1fr/);
});
});
describe('Performance Tests', () => {
it('should load pages within acceptable time limits', () => {
const pages = [
'/',
'/posts',
'/dashboard',
'/profile'
];
pages.forEach(page => {
cy.visit(page);
// Check page load time
cy.window().then((win) => {
const loadTime = win.performance.timing.loadEventEnd - win.performance.timing.navigationStart;
expect(loadTime).to.be.lessThan(3000); // 3 seconds
});
});
});
it('should handle large lists efficiently', () => {
// Create many posts
for (let i = 0; i < 100; i++) {
cy.createPost(`Post ${i}`, `Content for post ${i}`, ['test']);
}
cy.visit('/posts');
// Should implement pagination or virtual scrolling
cy.get('[data-testid="posts-list"]').should('be.visible');
// Should not load all 100 posts at once
cy.get('[data-testid="post-item"]').should('have.length.lessThan', 50);
// Test pagination
cy.get('[data-testid="pagination"]').should('be.visible');
cy.get('[data-testid="next-page"]').click();
cy.url().should('include', 'page=2');
});
});
describe('Accessibility Tests', () => {
it('should be accessible to keyboard users', () => {
// Test keyboard navigation
cy.get('body').tab();
cy.focused().should('have.attr', 'data-testid', 'skip-to-content');
// Navigate through main navigation
cy.get('[data-testid="nav-home"]').focus();
cy.focused().should('have.attr', 'data-testid', 'nav-home');
// Test form accessibility
cy.visit('/register');
cy.get('[data-testid="username-input"]').focus();
cy.focused().should('have.attr', 'data-testid', 'username-input');
// Check for proper labels
cy.get('[data-testid="username-input"]').should('have.attr', 'aria-label');
cy.get('[data-testid="password-input"]').should('have.attr', 'aria-label');
});
it('should have proper ARIA attributes', () => {
cy.visit('/posts');
// Check for proper ARIA roles
cy.get('[data-testid="main-content"]').should('have.attr', 'role', 'main');
cy.get('[data-testid="navigation"]').should('have.attr', 'role', 'navigation');
// Check for ARIA labels
cy.get('[data-testid="search-input"]').should('have.attr', 'aria-label', 'Search posts');
cy.get('[data-testid="search-button"]').should('have.attr', 'aria-label', 'Search');
// Check for ARIA descriptions
cy.get('[data-testid="form-help"]').should('have.attr', 'aria-describedby');
});
});
describe('Error Handling', () => {
it('should handle network errors gracefully', () => {
// Simulate network failure
cy.intercept('POST', '/api/v1/posts', { forceNetworkError: true });
cy.login('testuser', 'SecurePassword123!');
cy.visit('/posts/create');
// Fill and submit form
cy.get('[data-testid="title-input"]').type('Test Post');
cy.get('[data-testid="content-textarea"]').type('Test content');
cy.get('[data-testid="create-post-button"]').click();
// Should show error message
cy.get('[data-testid="error-message"]')
.should('be.visible')
.and('contain', 'Network error');
});
it('should handle 404 errors', () => {
cy.visit('/non-existent-page', { failOnStatusCode: false });
// Should show 404 page
cy.get('[data-testid="error-page"]').should('be.visible');
cy.get('[data-testid="error-code"]').should('contain', '404');
cy.get('[data-testid="error-message"]').should('contain', 'Page not found');
});
it('should handle server errors', () => {
cy.intercept('GET', '/api/v1/posts', { statusCode: 500 });
cy.visit('/posts');
// Should show server error message
cy.get('[data-testid="error-message"]')
.should('be.visible')
.and('contain', 'Server error');
});
});
});
// Custom commands for reusable actions
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login');
cy.get('[data-testid="username-input"]').type(username);
cy.get('[data-testid="password-input"]').type(password);
cy.get('[data-testid="login-button"]').click();
cy.url().should('include', '/dashboard');
});
Cypress.Commands.add('createPost', (title, content, tags = []) => {
cy.visit('/posts/create');
cy.get('[data-testid="title-input"]').type(title);
cy.get('[data-testid="content-textarea"]').type(content);
tags.forEach(tag => {
cy.get('[data-testid="tag-input"]').type(`${tag}{enter}`);
});
cy.get('[data-testid="create-post-button"]').click();
cy.url().should('match', /\/posts\/\d+/);
});
// cypress.config.js
module.exports = {
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.js',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
pageLoadTimeout: 30000,
env: {
username: 'testuser',
password: 'SecurePassword123!'
},
setupNodeEvents(on, config) {
// Custom task to clear test database
on('task', {
clearTestDb() {
// Implementation to clear test database
return null;
},
seedTestDb() {
// Implementation to seed test database
return null;
}
});
}
}
};
Software Testing Pyramide
Test Levels
graph TD
A[E2E Tests] --> B[Integration Tests]
B --> C[Unit Tests]
A1[<br/>Schnelle Tests<br/>Hohe Abdeckung<br/>Günstig] --> C
B1[<br/>Mittel<br/>Mittel<br/>Mittel] --> B
C1[<br/>Langsame Tests<br/>Geringe Abdeckung<br/>Teuer] --> A
style A fill:#ff9999
style B fill:#ffcc99
style C fill:#99ff99
Test Frameworks Vergleich
JavaScript Testing Frameworks
| Framework | Typ | Features | Anwendung |
|---|---|---|---|
| Jest | Unit/Integration | All-in-One, Snapshot Testing | React/Node.js |
| Mocha | Unit | Flexible, Chai Assertion | Node.js/Browser |
| Cypress | E2E | All-in-One, Time Travel | Web Applications |
| Playwright | E2E | Multi-Browser, Parallel | Web Applications |
Python Testing Frameworks
| Framework | Typ | Features | Anwendung |
|---|---|---|---|
| PyTest | Unit/Integration | Fixtures, Parametrized Tests | Python Applications |
| Unittest | Unit | Built-in, xUnit Style | Python Standard Library |
| Selenium | E2E | Browser Automation | Web Applications |
| Robot Framework | E2E | Keyword-Driven | Test Automation |
TDD vs. BDD
Test-Driven Development (TDD)
graph LR
A[Red] --> B[Green]
B --> C[Refactor]
C --> A
A1[Schreiter fehlenden Test] --> A
B1[Implementiere minimalen Code] --> B
C1[Verbessere Code-Qualität] --> C
Behavior-Driven Development (BDD)
Feature: User Registration
As a new user
I want to register an account
So that I can access the application
Scenario: Successful registration
Given I am on the registration page
When I fill in valid registration details
And I submit the registration form
Then I should see a success message
And I should receive a confirmation email
And my account should be created
Vorteile und Nachteile
Vorteile von Software Testing
- Qualitätssicherung: Frühe Fehlererkennung
- Regression-Verhinderung: Automatisierte Testsuite
- Dokumentation: Tests als lebende Dokumentation
- Refactoring-Sicherheit: Vertrauen bei Code-Änderungen
- Kostenreduktion: Geringere Fehlerbehebungskosten
Nachteile
- Zeitaufwand: Testentwicklung benötigt Zeit
- Wartung: Tests müssen mit Code gewartet werden
- Komplexität: Komplexe Testsetups
- False Positives: Tests können fehlschlagen
- Lernkurve: Test-Frameworks erlernen
Häufige Prüfungsfragen
-
Was ist der Unterschied zwischen Unit Tests und Integration Tests? Unit Tests testen isolierte Komponenten, Integration Tests testen die Zusammenarbeit mehrerer Komponenten.
-
Erklären Sie das TDD Red-Green-Refactor Zyklus! Red: Schreibe einen fehlenden Test, Green: Implementiere minimalen Code, Refactor: Verbessere Code-Qualität.
-
Wann verwendet man E2E Tests? E2E Tests werden für komplette Benutzer-Workflows verwendet, um das Gesamtsystem zu validieren.
-
Was sind die Vorteile von BDD? BDD verbessert die Kommunikation zwischen Entwicklern und Stakeholdern und erstellt lesbare, verständliche Tests.