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.

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

  1. Legalidad: Cumplir siempre con RGPD y LOPDGDD
  2. Ética: Priorizar la privacidad y el consentimiento
  3. Seguridad: Implementar controles técnicos robustos
  4. Trazabilidad: Documentar y auditar todo el proceso
  5. 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.