Generar DNI para Testing: Buenas Prácticas y Consideraciones Legales
Aprende a generar DNI válidos para pruebas de software de forma ética y legal. Metodologías, herramientas y consideraciones de privacidad.
¿Por Qué Necesitamos DNI para Testing?
En el desarrollo de aplicaciones que manejan datos personales, es fundamental realizar pruebas exhaustivas sin comprometer la privacidad de usuarios reales. Los DNI de testing permiten validar funcionalidades, interfaces y procesos de negocio manteniendo el cumplimiento legal y ético.
Marco Legal y Consideraciones Éticas
RGPD y Protección de Datos
El Reglamento General de Protección de Datos establece principios claros:
- Minimización de datos: Solo procesar datos necesarios
- Pseudonimización: Usar identificadores que no permitan identificación directa
- Protección desde el diseño: Implementar salvaguardas técnicas
- Derecho al olvido: Capacidad de eliminar datos de prueba
LOPDGDD Española
La Ley Orgánica de Protección de Datos española añade:
- Prohibición de usar datos reales sin consentimiento
- Obligación de usar datos sintéticos cuando sea posible
- Responsabilidad del encargado del tratamiento
- Notificación de brechas de seguridad
Consideraciones Éticas
- Nunca usar DNI reales sin consentimiento explícito
- Generar datos sintéticos que cumplan reglas de negocio
- Documentar el propósito de cada conjunto de datos de prueba
- Implementar caducidad automática de datos de testing
Metodologías de Generación Segura
Algoritmos Matemáticamente Válidos
function generarDNITesting() {
const letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
// Generar número aleatorio válido
const numero = Math.floor(Math.random() * (99999999 - 10000000) + 10000000);
// Calcular letra de control
const letra = letras[numero % 23];
return {
dni: `${numero}${letra}`,
numero: numero,
letra: letra,
esValido: true,
esTesting: true,
timestamp: new Date().toISOString()
};
}
Rangos Reservados para Testing
class DNITestingGenerator:
# Rangos reservados que no coinciden con DNI reales
RANGOS_TESTING = [
(90000000, 90999999), # Rango específico para testing
(80000000, 80999999), # Rango alternativo
(70000000, 70999999) # Rango para casos especiales
]
LETRAS = 'TRWAGMYFPDXBNJZSQVHLCKE'
def generar_dni_testing(self, rango_index=0):
if rango_index >= len(self.RANGOS_TESTING):
raise ValueError("Índice de rango inválido")
inicio, fin = self.RANGOS_TESTING[rango_index]
numero = random.randint(inicio, fin)
letra = self.LETRAS[numero % 23]
return {
'dni': f"{numero}{letra}",
'numero': numero,
'letra': letra,
'rango': f"{inicio}-{fin}",
'es_testing': True,
'generado': datetime.now().isoformat()
}
Identificadores Únicos de Testing
class TestingIDManager {
constructor() {
this.used_ids = new Set();
this.prefix = 'TEST_';
}
generateUniqueDNI() {
let dni;
let attempts = 0;
const maxAttempts = 1000;
do {
if (attempts++ > maxAttempts) {
throw new Error('No se pudo generar DNI único');
}
const numero = this.generateTestNumber();
const letra = this.calculateControlLetter(numero);
dni = `${numero}${letra}`;
} while (this.used_ids.has(dni));
this.used_ids.add(dni);
return {
dni: dni,
id_testing: `${this.prefix}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 24h
};
}
generateTestNumber() {
// Usar rango específico para testing
return Math.floor(Math.random() * 1000000) + 90000000;
}
calculateControlLetter(numero) {
const letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
return letras[numero % 23];
}
}
Herramientas y Bibliotecas
Faker.js para JavaScript
const faker = require('faker/locale/es');
class DNIFaker {
static create() {
// Configurar Faker para España
faker.locale = 'es';
const numero = faker.datatype.number({ min: 90000000, max: 90999999 });
const letra = this.calculateLetter(numero);
return {
dni: `${numero}${letra}`,
persona: {
nombre: faker.name.firstName(),
apellidos: `${faker.name.lastName()} ${faker.name.lastName()}`,
email: faker.internet.email(),
telefono: faker.phone.phoneNumber(),
direccion: {
calle: faker.address.streetAddress(),
ciudad: faker.address.city(),
cp: faker.address.zipCode(),
provincia: faker.address.state()
}
},
metadata: {
generado: new Date().toISOString(),
es_testing: true,
faker_version: faker.version
}
};
}
static calculateLetter(numero) {
const letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
return letras[numero % 23];
}
}
Factory Boy para Python
import factory
from datetime import datetime, timedelta
class DNITestingFactory(factory.Factory):
class Meta:
model = dict
numero = factory.LazyFunction(
lambda: random.randint(90000000, 90999999)
)
letra = factory.LazyAttribute(
lambda obj: 'TRWAGMYFPDXBNJZSQVHLCKE'[obj.numero % 23]
)
dni = factory.LazyAttribute(
lambda obj: f"{obj.numero}{obj.letra}"
)
es_testing = True
generado = factory.LazyFunction(datetime.now)
expira = factory.LazyFunction(
lambda: datetime.now() + timedelta(hours=24)
)
# Datos personales sintéticos
nombre = factory.Faker('first_name', locale='es_ES')
apellidos = factory.LazyFunction(
lambda: f"{factory.Faker('last_name', locale='es_ES').generate()} {factory.Faker('last_name', locale='es_ES').generate()}"
)
email = factory.LazyAttribute(
lambda obj: f"{obj.nombre.lower()}.{obj.apellidos.split()[0].lower()}@testing.local"
)
# Uso
test_person = DNITestingFactory()
print(test_person)
Patrones de Testing por Escenarios
Testing de Validación
class ValidationTestSuite {
static getTestCases() {
return {
validos: [
{ dni: '90123456P', descripcion: 'DNI válido rango testing' },
{ dni: '90987654H', descripcion: 'DNI válido límite superior' },
{ dni: '90000001R', descripcion: 'DNI válido límite inferior' }
],
invalidos: [
{ dni: '9012345P', descripcion: 'Número incompleto' },
{ dni: '901234567P', descripcion: 'Número excesivo' },
{ dni: '90123456X', descripcion: 'Letra incorrecta' },
{ dni: 'ABCD1234P', descripcion: 'Caracteres no numéricos' },
{ dni: '', descripcion: 'Cadena vacía' },
{ dni: null, descripcion: 'Valor nulo' }
],
extremos: [
{ dni: '90000000T', descripcion: 'Número mínimo del rango' },
{ dni: '90999999R', descripcion: 'Número máximo del rango' }
]
};
}
static generateStressTest(cantidad = 1000) {
const dnis = [];
const generator = new DNITestingGenerator();
for (let i = 0; i < cantidad; i++) {
dnis.push(generator.generar_dni_testing());
}
return dnis;
}
}
Testing de Rendimiento
class PerformanceTestData:
@staticmethod
def generate_load_test_data(size=10000):
"""Genera datos para pruebas de carga"""
generator = DNITestingGenerator()
start_time = time.time()
dnis = []
for i in range(size):
dni_data = generator.generar_dni_testing()
dni_data['batch_id'] = f"LOAD_TEST_{int(start_time)}"
dni_data['sequence'] = i
dnis.append(dni_data)
end_time = time.time()
return {
'dnis': dnis,
'metadata': {
'total_generated': size,
'generation_time': end_time - start_time,
'rate_per_second': size / (end_time - start_time),
'batch_id': f"LOAD_TEST_{int(start_time)}"
}
}
@staticmethod
def generate_concurrent_test_data(threads=10, per_thread=100):
"""Genera datos para pruebas de concurrencia"""
import concurrent.futures
import threading
results = []
thread_local = threading.local()
def generate_batch(thread_id):
if not hasattr(thread_local, 'generator'):
thread_local.generator = DNITestingGenerator()
batch = []
for i in range(per_thread):
dni_data = thread_local.generator.generar_dni_testing()
dni_data['thread_id'] = thread_id
dni_data['sequence'] = i
batch.append(dni_data)
return batch
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
futures = [executor.submit(generate_batch, i) for i in range(threads)]
for future in concurrent.futures.as_completed(futures):
results.extend(future.result())
return results
Gestión del Ciclo de Vida
Expiración Automática
class TestDataLifecycle {
constructor(redis_client) {
this.redis = redis_client;
this.default_ttl = 24 * 60 * 60; // 24 horas
}
async storeTestDNI(dni_data, ttl = null) {
const key = `test_dni:${dni_data.dni}`;
const expiration = ttl || this.default_ttl;
await this.redis.setex(
key,
expiration,
JSON.stringify({
...dni_data,
stored_at: new Date().toISOString(),
expires_at: new Date(Date.now() + expiration * 1000).toISOString()
})
);
return key;
}
async cleanupExpired() {
const pattern = 'test_dni:*';
const keys = await this.redis.keys(pattern);
let cleaned = 0;
for (const key of keys) {
const ttl = await this.redis.ttl(key);
if (ttl <= 0) {
await this.redis.del(key);
cleaned++;
}
}
return {
total_keys: keys.length,
cleaned: cleaned,
remaining: keys.length - cleaned
};
}
}
Auditoría y Trazabilidad
class TestDataAudit:
def __init__(self, database_connection):
self.db = database_connection
def log_generation(self, dni_data, context):
"""Registra la generación de datos de prueba"""
audit_record = {
'dni': dni_data['dni'],
'generated_at': datetime.now(),
'context': context,
'generator_version': '1.0.0',
'user_id': context.get('user_id'),
'test_suite': context.get('test_suite'),
'purpose': context.get('purpose', 'general_testing')
}
self.db.audit_log.insert_one(audit_record)
return audit_record
def log_usage(self, dni, action, result):
"""Registra el uso de datos de prueba"""
usage_record = {
'dni': dni,
'action': action,
'result': result,
'timestamp': datetime.now(),
'ip_address': self._get_client_ip(),
'user_agent': self._get_user_agent()
}
self.db.usage_log.insert_one(usage_record)
return usage_record
def generate_compliance_report(self, start_date, end_date):
"""Genera reporte de cumplimiento"""
pipeline = [
{
'$match': {
'generated_at': {
'$gte': start_date,
'$lte': end_date
}
}
},
{
'$group': {
'_id': '$purpose',
'count': {'$sum': 1},
'users': {'$addToSet': '$user_id'}
}
}
]
return list(self.db.audit_log.aggregate(pipeline))
Integración con Frameworks de Testing
Jest Configuration
// jest.config.js
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/test-data/**/*.js'
],
globals: {
TEST_DNI_RANGE: [90000000, 90999999],
ENABLE_DNI_GENERATION: true
}
};
// tests/setup.js
const DNITestingGenerator = require('../src/dni-testing-generator');
global.generateTestDNI = () => {
const generator = new DNITestingGenerator();
return generator.generar_dni_testing();
};
beforeEach(() => {
// Limpiar datos de prueba antes de cada test
if (global.testDataCleanup) {
global.testDataCleanup();
}
});
Pytest Configuration
# conftest.py
import pytest
from dni_testing_generator import DNITestingGenerator
@pytest.fixture
def dni_generator():
return DNITestingGenerator()
@pytest.fixture
def test_dni(dni_generator):
"""Genera un DNI válido para testing"""
return dni_generator.generar_dni_testing()
@pytest.fixture
def test_dni_batch(dni_generator):
"""Genera un lote de DNI para testing"""
return [dni_generator.generar_dni_testing() for _ in range(10)]
@pytest.fixture(autouse=True)
def cleanup_test_data():
"""Limpia datos de prueba después de cada test"""
yield
# Código de limpieza
pass
# Marcadores personalizados
pytest_mark = [
"slow: marca tests lentos",
"integration: marca tests de integración",
"dni_validation: marca tests de validación DNI"
]
Mejores Prácticas de Implementación
Separación de Entornos
# docker-compose.testing.yml
version: '3.8'
services:
app-testing:
build: .
environment:
- NODE_ENV=testing
- DNI_GENERATION_ENABLED=true
- DNI_TEST_RANGE_START=90000000
- DNI_TEST_RANGE_END=90999999
- DATA_RETENTION_HOURS=24
volumes:
- ./test-data:/app/test-data
networks:
- testing-network
redis-testing:
image: redis:alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
networks:
- testing-network
networks:
testing-network:
driver: bridge
Configuración de Desarrollo
// config/testing.js
module.exports = {
dni: {
generation: {
enabled: process.env.NODE_ENV !== 'production',
ranges: [
{ start: 90000000, end: 90999999, purpose: 'general' },
{ start: 80000000, end: 80999999, purpose: 'integration' },
{ start: 70000000, end: 70999999, purpose: 'performance' }
],
default_ttl: 24 * 60 * 60, // 24 horas
max_per_session: 1000
},
validation: {
strict_mode: true,
allow_real_dni: false,
log_attempts: true
}
},
security: {
rate_limit: {
window_ms: 15 * 60 * 1000, // 15 minutos
max_requests: 100
},
audit: {
enabled: true,
retention_days: 30,
log_level: 'info'
}
}
};
Conclusiones y Recomendaciones
Principios Fundamentales
- Legalidad: Cumplir siempre con RGPD y LOPDGDD
- Ética: Priorizar la privacidad y el consentimiento
- Seguridad: Implementar controles técnicos robustos
- Trazabilidad: Documentar y auditar todo el proceso
- Temporalidad: Implementar caducidad automática
Lista de Verificación
- ✅ Usar rangos específicos para testing
- ✅ Implementar expiración automática
- ✅ Documentar propósito y contexto
- ✅ Separar entornos de desarrollo y producción
- ✅ Auditar generación y uso
- ✅ Formar al equipo en buenas prácticas
- ✅ Revisar cumplimiento regularmente
La generación responsable de DNI para testing es esencial para el desarrollo de software que maneje datos personales, garantizando tanto la funcionalidad como el cumplimiento legal y ético.