Fundamentos de Testing en Software: Unit Tests, Integration Tests, E2E Tests, TDD y BDD
Este artículo es una introducción completa a los fundamentos de testing en software, incluyendo Unit Tests, Integration Tests, E2E Tests, TDD y BDD con ejemplos prácticos.
En Resumen
El testing en software es un proceso sistemático para verificar la calidad y funcionalidad de una aplicación, asegurando que cumple con los requisitos y funciona sin errores.
Descripción Técnica Concisa
Testing en software es la verificación de una aplicación respecto a su corrección, completitud y calidad mediante pruebas dirigidas y métodos de validación.
Componentes principales:
Unit Tests
- Tests aislados: Prueba componentes individuales de forma independiente
- Ejecución rápida: Milisegundos a pocos segundos
- Mocking: Simula dependencias externas
- Coverage: Mide la cobertura de código
- Frameworks: JUnit, Jest, PyTest
Integration Tests
- Integración de componentes: Prueba múltiples módulos en conjunto
- Tests de base de datos: Valida la capa de persistencia
- Tests de API: Verifica interfaces entre servicios
- Integración del sistema: Comprueba el sistema completo
- Entornos de prueba: Sistemas staging y de testing
End-to-End (E2E) Tests
- Escenarios de usuario: Prueba flujos completos
- Automatización de navegador: Simula interacciones en la UI
- Cross-browser: Prueba en diferentes navegadores
- Tests móviles: Aplicaciones iOS/Android
- Herramientas: Selenium, Cypress, Playwright
Test-Driven Development (TDD)
- Red-Green-Refactor: Ciclo iterativo de desarrollo
- Tests primero: Escribe pruebas antes de la implementación
- Pasos pequeños: Desarrollo incremental
- Tests de regresión: Suite de pruebas automatizada
- Mejora del diseño: Arquitectura de software más robusta
Behavior-Driven Development (BDD)
- Sintaxis Gherkin: Descripciones de pruebas legibles
- User Stories: Formula requisitos del negocio
- Comunicación con stakeholders: Entendimiento compartido
- Cucumber: Implementación del framework BDD
- Documentación viva: Documentación actualizada continuamente
Puntos Clave para Examen
- Unit Tests: Pruebas aisladas de funciones/clases individuales
- Integration Tests: Pruebas de cómo trabajan juntos los componentes
- E2E Tests: Flujos completos del usuario de principio a fin
- TDD: Test-Driven Development con ciclo Red-Green-Refactor
- BDD: Behavior-Driven Development con sintaxis Gherkin
- Test Pyramid: Más Unit Tests que Integration/E2E Tests
- Mocking: Simulación de dependencias para las pruebas
- Code Coverage: Medición de la cobertura de pruebas
- IHK-relevante: Aseguramiento de calidad de software y estrategias de testing
Componentes Fundamentales
- Test Strategy: Planificación y priorización de pruebas
- Test Automation: Ejecución automatizada de pruebas
- Test Frameworks: Herramientas y bibliotecas para testing
- Continuous Testing: Integración en pipelines CI/CD
- Test Data Management: Generación y gestión de datos de prueba
- Test Reporting: Análisis y documentación de resultados
- Performance Testing: Pruebas de carga y estrés
- Security Testing: Identificación de vulnerabilidades
Ejemplos Prácticos
1. Unit Tests con JavaScript y 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. Pruebas de integración con Python y 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. Pruebas End-to-End con 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;
}
});
}
}
};
Pirámide de Pruebas de Software
Niveles de Pruebas
graph TD
A[E2E Tests] --> B[Integration Tests]
B --> C[Unit Tests]
A1[<br/>Pruebas Rápidas<br/>Cobertura Alta<br/>Económicas] --> C
B1[<br/>Medio<br/>Medio<br/>Medio] --> B
C1[<br/>Pruebas Lentas<br/>Cobertura Baja<br/>Costosas] --> A
style A fill:#ff9999
style B fill:#ffcc99
style C fill:#99ff99
Comparativa de Frameworks de Pruebas
Frameworks de Testing en JavaScript
| Framework | Tipo | Características | Aplicación |
|---|---|---|---|
| Jest | Unit/Integration | Todo en uno, Snapshot Testing | React/Node.js |
| Mocha | Unit | Flexible, Chai Assertion | Node.js/Browser |
| Cypress | E2E | Todo en uno, Time Travel | Aplicaciones Web |
| Playwright | E2E | Multi-navegador, Ejecución Paralela | Aplicaciones Web |
Frameworks de Testing en Python
| Framework | Tipo | Características | Aplicación |
|---|---|---|---|
| PyTest | Unit/Integration | Fixtures, Pruebas Parametrizadas | Aplicaciones Python |
| Unittest | Unit | Integrado, Estilo xUnit | Librería Estándar Python |
| Selenium | E2E | Automatización de Navegadores | Aplicaciones Web |
| Robot Framework | E2E | Conducido por Palabras Clave | Automatización de Pruebas |
TDD vs. BDD
Test-Driven Development (TDD)
graph LR
A[Red] --> B[Green]
B --> C[Refactor]
C --> A
A1[Escribe un test que falla] --> A
B1[Implementa código mínimo] --> B
C1[Mejora la calidad del código] --> 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
Ventajas e Inconvenientes
Ventajas del Testing de Software
- Garantía de Calidad: Detección temprana de errores
- Prevención de Regresiones: Suite de pruebas automatizada
- Documentación: Las pruebas actúan como documentación viva
- Seguridad en Refactorización: Confianza al hacer cambios en el código
- Reducción de Costos: Menores gastos en corrección de errores
Inconvenientes
- Inversión de Tiempo: El desarrollo de pruebas requiere tiempo
- Mantenimiento: Las pruebas deben actualizarse con el código
- Complejidad: Configuración de pruebas complejas
- Falsos Positivos: Las pruebas pueden fallar sin razón
- Curva de Aprendizaje: Necesario aprender los frameworks
Preguntas Frecuentes de Examen
-
¿Cuál es la diferencia entre Unit Tests e Integration Tests? Unit Tests prueban componentes aislados, mientras que Integration Tests validan la interacción entre múltiples componentes.
-
Explica el ciclo Red-Green-Refactor del TDD. Red: Escribe una prueba que falla, Green: Implementa código mínimo para pasar la prueba, Refactor: Mejora la calidad del código.
-
¿Cuándo utilizamos E2E Tests? Los E2E Tests se usan para flujos de usuario completos, validando el funcionamiento del sistema en su totalidad.
-
¿Cuáles son las ventajas de BDD? BDD mejora la comunicación entre desarrolladores y stakeholders, además de producir pruebas legibles y comprensibles.
Continúa en la Ruta de Aprendizaje de Testing de Software
El siguiente artículo en la ruta de aprendizaje de Testing de Software cubre Pirámide de pruebas: Unit, Integration y E2E Tests en proporción — cómo balancear adecuadamente las diferentes capas de pruebas.



