← Voltar ao blog

Gerador de Placa Mercosul para Testes: Algoritmo e Validação

·11 min de leitura

Placas Mercosul seguem um padrão determinístico introduzido no Brasil em 2018. Isso significa que gerar placas válidas para testes não é difícil — mas gerar placas que passem por todos os validadores de um sistema real exige conhecer as restrições exatas do Denatran.

O formato Mercosul tem estrutura determinística — e isso facilita a geração

Padrão ABC1D23: três letras, um dígito, uma letra, dois dígitos

O padrão Mercosul para carros de passeio segue [A-Z]{3}[0-9][A-Z][0-9]{2}, sete caracteres sem hífen. A posição 5 (índice 4) é o diferenciador entre o formato antigo e o novo: se for letra, é Mercosul. Se for dígito, é placa antiga.

Exemplos válidos: BRA2E19, ABC1D23, XYZ9K00.

Motos seguem variante ABC1D2 (seis caracteres)

Motos têm apenas seis caracteres: [A-Z]{3}[0-9][A-Z][0-9]. A lógica de geração é idêntica, com o bloco numérico final tendo apenas um dígito em vez de dois.

Por que o Denatran excluiu certas letras (I, O, Q) do bloco alfabético

O Denatran excluiu I, O e Q dos blocos alfabéticos para evitar confusão visual com os dígitos 1, 0 e 0/Q em fontes condensadas. Essa exclusão se aplica tanto às três primeiras letras quanto à letra na posição 5. O alfabeto efetivo tem 23 letras.

Restrições reais que o algoritmo precisa respeitar

Sequências proibidas: palavras obscenas e siglas de órgãos públicos (Resolução Contran nº 752/2018)

A Resolução Contran nº 752/2018 define que o Denatran mantém uma lista interna de combinações proibidas. Não há publicação oficial completa da lista, mas o princípio é claro: o bloco inicial de três letras não pode formar palavras de baixo calão ou siglas sensíveis. Para um gerador de testes, basta uma blocklist mínima:

const BLOCKED_PREFIXES = new Set([
  'SSP', 'PRF', 'DPF', 'PGR', 'STF', 'STJ', 'CNJ',
]);

O segundo bloco tem exatamente um caractere alfabético na posição 5

Essa é a restrição que define o formato Mercosul. O regex completo do segundo bloco é [0-9][A-HJ-NPR-Z][0-9]{1,2}. A classe [A-HJ-NPR-Z] já exclui I, O e Q.

Diferença entre geração aleatória e geração progressiva (como o Detran sequencia)

O Detran emite placas sequencialmente por UF. Para testes, geração aleatória é suficiente e mais simples. Geração progressiva só faz sentido se o sistema testado valida sequências ou prefixos regionais específicos.

Implementação do gerador em TypeScript

Função generateMercosulPlate() pura e sem dependências externas

const ALPHA = 'ABCDEFGHJKLMNPRSTUVWXYZ'; // 23 letras, sem I, O, Q
const DIGITS = '0123456789';

function randomChar(str: string): string {
  return str[Math.floor(Math.random() * str.length)];
}

const BLOCKED_PREFIXES = new Set([
  'SSP', 'PRF', 'DPF', 'PGR', 'STF', 'STJ', 'CNJ',
]);

export function generateMercosulPlate(
  options: { format?: 'car' | 'moto' } = {}
): string {
  const { format = 'car' } = options;

  let prefix: string;
  do {
    prefix = randomChar(ALPHA) + randomChar(ALPHA) + randomChar(ALPHA);
  } while (BLOCKED_PREFIXES.has(prefix));

  const digit1  = randomChar(DIGITS);
  const letter  = randomChar(ALPHA);
  const suffix  = format === 'moto'
    ? randomChar(DIGITS)
    : randomChar(DIGITS) + randomChar(DIGITS);

  return `${prefix}${digit1}${letter}${suffix}`;
}

export function formatPlate(plate: string, hyphen = false): string {
  if (!hyphen) return plate;
  return `${plate.slice(0, 3)}-${plate.slice(3)}`;
}

Gerando o bloco alfabético inicial com exclusão de I, O, Q

A constante ALPHA com 23 letras resolve isso diretamente. O loop do...while descarta prefixos bloqueados antes de retornar — na prática, o loop raramente itera mais de uma vez, dado o espaço de 23³ = 12.167 combinações possíveis.

Gerando a parte numérica com posição 5 variável A–Z (exceto I, O, Q)

O mesmo ALPHA cobre a posição 5. Não há pesos ou distribuições específicas documentadas publicamente para essa posição, então distribuição uniforme é a abordagem correta para testes.

Validação via regex e função de checagem

Regex definitivo para placas Mercosul e antigas no mesmo validador

FormatoRegexExemplo
Mercosul carro^[A-Z]{3}\d[A-HJ-NPR-Z]\d{2}$BRA2E19
Mercosul moto^[A-Z]{3}\d[A-HJ-NPR-Z]\d$BRA2E1
Placa antiga^[A-Z]{3}\d{4}$ABC1234

Função validatePlate(plate: string) com retorno tipado

type PlateFormat = 'mercosul' | 'antiga' | 'moto' | 'invalid';

const PATTERNS: Record<Exclude<PlateFormat, 'invalid'>, RegExp> = {
  mercosul: /^[A-Z]{3}\d[A-HJ-NPR-Z]\d{2}$/,
  moto:     /^[A-Z]{3}\d[A-HJ-NPR-Z]\d$/,
  antiga:   /^[A-Z]{3}\d{4}$/,
};

export function validatePlate(
  plate: string
): { valid: boolean; format: PlateFormat } {
  const normalized = plate.toUpperCase().replace(/[\s-]/g, '');

  for (const [format, regex] of Object.entries(PATTERNS)) {
    if (regex.test(normalized)) {
      return { valid: true, format: format as PlateFormat };
    }
  }
  return { valid: false, format: 'invalid' };
}

Casos de borda: placas com espaço, lowercase, hífen inconsistente

A normalização toUpperCase().replace(/[\s-]/g, '') cobre os três casos. Placas recebidas de formulários raramente chegam no formato canônico, e a função acima absorve todas as variações comuns.

Dados de veículo correlacionados para testes mais realistas

Associar placa a UF de emplacamento (prefixo de três letras tem distribuição regional real)

O Denatran distribui faixas de prefixo por UF. A tabela abaixo é uma aproximação suficiente para testes:

UFFaixa de prefixo
SPAAA–AZZ
RJBAA–BZZ
MGCAA–CZZ
RSDAA–DZZ
PREAA–EZZ

Use um mapa de prefixo inicial por UF em vez de tentar replicar a tabela exata do Detran — para testes, correlação aproximada é suficiente.

Combinar com generatePerson() para simular proprietário

Use o gerador de pessoa do FakeForge para gerar nome, CPF e endereço do titular em um único objeto. O gerador de CEP correlacionado com a UF torna o fixture ainda mais realista:

import { generateMercosulPlate, generateRenavam } from './plate';
import { generatePerson } from './person'; // FakeForge

function createVehicleOwner() {
  return {
    plate:   generateMercosulPlate(),
    renavam: generateRenavam(),
    owner:   generatePerson(),
  };
}

Gerar RENAVAM fake com dígito verificador correto (mod 11)

O RENAVAM tem 11 dígitos. Os 10 primeiros são a base numérica; o último é o dígito verificador calculado por mod 11 com pesos específicos:

export function generateRenavam(): string {
  const base = Array.from({ length: 10 }, () =>
    Math.floor(Math.random() * 10)
  );

  const weights = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
  const sum = base.reduce((acc, d, i) => acc + d * weights[i], 0);
  const remainder = sum % 11;
  const digit = remainder < 2 ? 0 : 11 - remainder;

  return [...base, digit].join('');
}

Integração com frameworks de teste

Factory com Vitest: criando fixtures de frota em massa

import { describe, it, expect } from 'vitest';
import { generateMercosulPlate, validatePlate } from './plate';
import { generateRenavam } from './renavam';

function createVehicleFixture(count: number) {
  return Array.from({ length: count }, () => ({
    plate:   generateMercosulPlate(),
    renavam: generateRenavam(),
  }));
}

describe('Vehicle fixtures', () => {
  it('generates 100 valid Mercosul plates', () => {
    const fleet = createVehicleFixture(100);
    for (const { plate } of fleet) {
      expect(validatePlate(plate).format).toBe('mercosul');
    }
  });
});

Usando a API REST do FakeForge como seed remoto em pipelines de CI

A API REST do FakeForge suporta geração em lote via query params:

curl "https://fakeforge.com.br/api/generate?type=placa&quantity=50&format=csv"

O plano Dev (R$29/mês) remove o limite de 100 chamadas/dia, adequado para pipelines que rodam a cada push.

Snapshot testing: por que fixar a semente do RNG evita falsos positivos

DICA: Substitua Math.random por um PRNG seedável (ex: seedrandom via npm) nos testes de snapshot. Em produção, use Math.random normalmente. Isso garante que o mesmo seed sempre produz os mesmos fixtures, eliminando falsos positivos por aleatoriedade.

Cobertura de cenários negativos é obrigatória

Placas antigas (ABC1234) que sistemas legados ainda aceitam

Sistemas de frota com mais de dez anos provavelmente têm registros no formato antigo. Gere placas antigas explicitamente para testar código legado:

function generateOldPlate(): string {
  const prefix = Array.from({ length: 3 }, () => randomChar(ALPHA)).join('');
  const nums   = Array.from({ length: 4 }, () => randomChar(DIGITS)).join('');
  return `${prefix}${nums}`;
}

Placas de coleção, diplomáticas e de teste (série ZZZ) — quando incluir

Placas diplomáticas (DDI-0001), de coleção e da série ZZZ são casos especiais que a maioria dos sistemas rejeita. Inclua-as na suite de testes negativos, não nos fixtures de dados válidos.

Fuzzing: gerando strings inválidas intencionais para testar boundary conditions

const FUZZ_CASES = [
  '',
  'ABC',
  'I00A111',    // I no bloco inicial — inválido
  'ABC O123',   // espaço e O proibido
  'ABC1D2',     // moto sem validação explícita de formato
  '!!!1A23',
];

for (const plate of FUZZ_CASES) {
  const result = validatePlate(plate);
  console.assert(result.format === 'invalid', `Esperado inválido: ${plate}`);
}

LGPD e dados de veículo: o que é pessoal e o que não é

Placa sozinha não identifica pessoa — mas placa + CPF + endereço forma dataset sensível (LGPD Art. 5º, I)

Uma placa isolada não é dado pessoal segundo a LGPD Art. 5º, I (Lei 13.709/2018 — texto integral). Ela não identifica nem torna identificável uma pessoa natural diretamente. A combinação placa + CPF + endereço + nome, no entanto, forma um conjunto identificador que se enquadra na definição de dado pessoal.

AVISO: Bancos de dados de teste que combinam dados reais de veículos com dados reais de pessoas — mesmo obtidos de fontes públicas — constituem tratamento de dados pessoais sem base legal clara. Use dados sintéticos em todas as camadas do pipeline de testes.

Banco de dados de testes com dados reais de veículos: risco jurídico e alternativa segura

Usar dumps de sistemas reais em ambiente de teste expõe a empresa ao Art. 46 da LGPD (obrigação de adotar medidas de segurança adequadas). A alternativa é gerar fixtures sintéticos com correlações realistas. Combine gerador de pessoa e gerador de CNPJ para montar frotas de pessoa física e jurídica sem nenhum dado real.

Diferença entre dado anonimizado e dado pseudonimizado no contexto de frota (LGPD Art. 12)

O LGPD Art. 12 define dado anonimizado como aquele que não pode ser revertido por "meio técnico razoável e disponível". Dado pseudonimizado — ex: CPF substituído por UUID, mas mapeável internamente — ainda é dado pessoal. Dados gerados sinteticamente são anonimizados por construção, sem custo adicional de processo.

Performance ao gerar grandes volumes

Geração de 10.000 placas únicas sem colisão: estratégia de Set vs. filtro probabilístico

export function generateUniquePlates(count: number): string[] {
  const seen = new Set<string>();
  while (seen.size < count) {
    seen.add(generateMercosulPlate());
  }
  return [...seen];
}

O espaço de placas Mercosul válidas é aproximadamente 23³ × 10 × 23 × 10² ≈ 280 milhões. Com 10.000 placas, a probabilidade de colisão é desprezível. Um Set simples é suficiente; filtro de Bloom só vale para volumes acima de 1 milhão.

Paralelismo com Promise.all e limites práticos de memória

Para volumes acima de 100.000, a geração síncrona em loop é mais eficiente do que Promise.all — o overhead de microtask supera qualquer ganho de paralelismo em operações CPU-bound. Quebre em chunks e libere o event loop entre eles:

async function generatePlatesInChunks(
  total: number,
  chunkSize = 10_000
): Promise<string[]> {
  const results: string[] = [];
  for (let i = 0; i < total; i += chunkSize) {
    const chunk = generateUniquePlates(Math.min(chunkSize, total - i));
    results.push(...chunk);
    await new Promise(r => setImmediate(r));
  }
  return results;
}

Export em CSV e SQL com CREATE TABLE veiculo pronto para carga

CREATE TABLE veiculo (
  id         SERIAL PRIMARY KEY,
  placa      CHAR(7)     NOT NULL,
  renavam    CHAR(11)    NOT NULL,
  uf         CHAR(2)     NOT NULL,
  created_at TIMESTAMP   DEFAULT NOW()
);

INSERT INTO veiculo (placa, renavam, uf) VALUES
  ('BRA2E19', '12345678901', 'SP'),
  ('XYZ9K00', '98765432109', 'RJ');

O export SQL da API REST do FakeForge gera o CREATE TABLE automaticamente com base no tipo solicitado.

Resumo

  • Implemente generateMercosulPlate() com o alfabeto de 23 letras (sem I, O, Q) e uma blocklist mínima de prefixos proibidos — o loop do...while descarta conflitos sem custo perceptível.
  • Use validatePlate() com suporte a Mercosul, moto e formato antigo; normalize case e hífens antes de aplicar qualquer regex.
  • Gere RENAVAM com dígito verificador mod 11 para fixtures de sistema de frota que precisam passar por validadores de documento.
  • Combine placa com gerador de pessoa e CEP por UF para dados correlacionados que passam por validadores de negócio, não apenas de formato.
  • Nunca use dados reais de veículos em ambiente de teste — a combinação com CPF e endereço forma dataset sujeito à LGPD Art. 5º, I, independentemente da fonte dos dados.
  • Próximo passo: adicionar CRV fake com número de registro, data de expedição e órgão emissor para testes de sistemas de documentação de frota completa.

Perguntas frequentes

Como gerar placa com prefixo de UF específica sem hardcodear toda a tabela do Detran?+

Mapeie UF à primeira letra do prefixo (SP→A, RJ→B, MG→C). Aleatorize as outras duas do ALPHA válido. Exemplo: `const p1 = ufMap[uf]; const p23 = randomChar(ALPHA) + randomChar(ALPHA); prefix = p1 + p23;` Não é sequencial como Detran real, mas passa validadores de correlação regional.

RENAVAM usa o mesmo algoritmo mod 11 do CPF?+

Não. RENAVAM usa 10 pesos (3,2,9,8,7,6,5,4,3,2); CPF usa dois módulos com 9 e 8 pesos. Cálculo final diverge: RENAVAM `remainder < 2 ? 0 : 11 - remainder`, CPF sempre `11 - remainder`. Reutilizar código de CPF gera dígitos inválidos.

Gerar 100 mil placas únicas com Set causa garbage collection lag perceptível?+

Não. 100k placas em Set ~0.04% do espaço válido (280M); colisões são raras. GC lag é imperceptível. Acima de 500k, chunking com `setImmediate()` reduz picos de memória. Teste em seu ambiente se for gerar milhões.

Meu validador legado rejeita placas Mercosul — devo gerar antigas em produção?+

Não force Mercosul em sistema que não suporta. Em testes, mantenha suites separadas: `testMercosulSupport()` usa `generateMercosulPlate()`, `testLegacyCompatibility()` usa `generateOldPlate()`. A migração do validador é responsabilidade da arquitetura, não do gerador.

Qual blocklist mínima protege contra rejeição em APIs de seguro ou Detran?+

SSP, PRF, DPF, PGR, STF, STJ, CNJ cobrem siglas de órgãos públicos (Resolução Contran 752/2018). Adicione palavrões se testar input real. APIs de terceiros podem ter listas maiores — solicite ao provedor ou use whitelist de prefixos conhecidos da UF ao invés de blocklist.