fiscal-rsfiscal-rs

Estratégia de Testes

Suítes de teste, categorias, padrões utilizados e como executar

Visão Geral

O fiscal-rs possui 739+ testes distribuídos em 16 arquivos de teste, além de 5 alvos de fuzzing. A maioria dos testes foi portada diretamente das suítes PHP (sped-nfe) e TypeScript do FinOpenPOS, garantindo paridade funcional entre as implementações.

Contribuição upstream ao sped-nfe

Durante o desenvolvimento do fiscal-rs, não apenas portamos os testes existentes — contribuímos 370 novos testes de volta ao sped-nfe original via PR #1313 (mergeado), elevando a cobertura de 40% para 86,5% (de 216 para 586 testes, de 896 para 2.274 assertions).

Os 5 arquivos contribuídos:

ArquivoTestesCobertura
TaxCoverageTest.php121Todos os branches ICMS CST/CSOSN, FCP, PIS/COFINS
RenderCoverageTest.php26NF-e/NFC-e render(), totais, transporte, pagamentos
CommunicationCoverageTest.php75Todos os métodos SEFAZ via SoapFake, QRCode, eventos
TraitsCoverageTest.php72Todos os traits que estavam com 0% de cobertura
DeepCoverageTest.php76Make render branches, Tools methods, edge cases

Esse processo revelou bugs latentes no sped-nfe que existiam há anos e foram corrigidos no caminho. Não é só paridade — é evolução mútua.

MétricaValor
Testes no fiscal-rs739+
Arquivos de teste16
Alvos de fuzzing5
Testes contribuídos ao sped-nfe370
Cobertura sped-nfe (antes → depois)40% → 86,5%
Fixtures (XML, certificados, schemas, TXT)4 diretórios

Categorias de Teste

Loading diagram...

Testes Portados

A maior parte da suíte (492 testes) foi portada linha a linha de implementações existentes em PHP e TypeScript. Cada bloco describe()/it() do TypeScript se tornou um mod/#[test] em Rust.

ArquivoTestesOrigem
make_ported_test.rs90make-ported.test.ts
deep_comm_coverage_ported_test.rs163Suite de cobertura PHP
tax_coverage_ported_part2_test.rs91Suite de cobertura PHP
misc_ported_test.rs61Utilitários PHP
tax_coverage_ported_test.rs45Cálculos tributários PHP
tools_ported_test.rs35Tools PHP
render_coverage_ported_test.rs28Renderização XML PHP
complement_ported_test.rs4Complementos PHP

Os testes portados validam que o fiscal-rs produz os mesmos resultados que as bibliotecas PHP e TypeScript originais, incluindo os mesmos XMLs de saída e os mesmos cálculos tributários.

Testes de Domínio

Testes escritos nativamente em Rust cobrindo módulos específicos:

  • tax_icms_test.rs (19 testes) -- Todos os CSTs de ICMS (00, 10, 20, 30, 40/41/50, 51, 60, 70, 90), CSOSNs do Simples Nacional (101, 102, 103, 201, 202, 500, 900) e ICMS interestadual (UF destino). Utiliza rstest para parametrização.
  • tax_pis_cofins_ipi_test.rs (17 testes) -- PIS, COFINS, IPI e Imposto de Importação
  • xml_builder_test.rs (21 testes) -- Construção de XML NF-e/NFC-e com o InvoiceBuilder
  • certificate_test.rs (14 testes) -- Extração de certificados PFX e assinatura digital XML
  • complement_test.rs (8 testes) -- Anexação de protocolo SEFAZ e inutilização
  • qrcode_test.rs (5 testes) -- Geração de QR codes v2.00 e v3.00

Snapshot Tests (insta)

Os 50 testes de snapshot utilizam a crate insta para verificar regressão de saída. Cobrem:

  • XML utils -- tag(), escape_xml(), extract_xml_tag_value()
  • Serialização de TaxElement -- ICMS00, PISAliq, COFINSAliq, IPITrib, II, ICMSUFDest
  • Formatação monetária -- format_cents, format_rate, format_rate4
  • Códigos de estado -- Mapeamento UF/IBGE para todos os 27 estados
  • Totais de ICMS -- create_icms_totals, merge_icms_totals

Os snapshots ficam armazenados em tests/snapshots/ e são comparados automaticamente. Para atualizar:

# Revisar mudanças de snapshot interativamente
cargo insta review

# Aceitar todas as mudanças de uma vez
cargo insta accept

A crate suporta dois modos de asserção:

// Snapshot armazenada em arquivo (tests/snapshots/)
assert_snapshot!(tag("xNome", &[], "Test Company".into()));

// Snapshot inline (valor esperado no próprio código)
assert_snapshot!(format_cents_2(1050), @"10.50");

Property Tests (proptest)

Os 41 testes de propriedade utilizam proptest para verificar invariantes com entradas geradas aleatoriamente. Organizados em 5 blocos proptest!:

MóduloPropriedades verificadas
format_utilsFormato monetário sempre tem ponto decimal e N casas; zero produz 0.000...; format_cents_or_zero(None) equivale a zero
xml_utilsescape_xml nunca contém caracteres especiais crus; strings seguras passam inalteradas; tag() abre com < e fecha com >
tag roundtripextract_xml_tag_value(tag(name, text)) retorna o texto original escapado
tax_elementSerialização com/sem outer_tag; filter_fields remove Nones; required_field(None) retorna erro
state_codesUFs desconhecidas retornam erro; códigos IBGE desconhecidos retornam erro; associatividade e comutatividade de merge_icms_totals

Exemplo de estratégia de geração:

fn reasonable_cents() -> impl Strategy<Value = i64> {
    -999_999_999i64..=999_999_999i64
}

fn safe_string() -> impl Strategy<Value = String> {
    "[a-zA-Z0-9 ]{1,50}"
}

Fuzz Targets (cargo-fuzz)

Os 5 alvos de fuzzing em fuzz/fuzz_targets/ testam a robustez contra entradas arbitrárias e potencialmente maliciosas:

AlvoO que faz fuzzing
fuzz_gtin_validationValidação de códigos GTIN (EAN-8, EAN-13, EAN-14)
fuzz_response_parsersParsers de respostas SOAP da SEFAZ
fuzz_txt_to_xmlConversão de layouts TXT legados para XML
fuzz_xml_standardizePadronização/normalização de XML
fuzz_xml_tag_extractionExtração de valores de tags XML
# Executar fuzzing (requer nightly)
cargo +nightly fuzz run fuzz_xml_tag_extraction

# Executar por tempo limitado (60 segundos)
cargo +nightly fuzz run fuzz_gtin_validation -- -max_total_time=60

Infraestrutura de Testes

Módulo Comum (tests/common/mod.rs)

Todos os testes de integração compartilham helpers via mod common;:

// Helpers de asserção XML
expect_xml_tag_values(xml, &[("tag", "valor")]);
expect_xml_contains(xml, &["substring1", "substring2"]);
expect_xml_not_contains(xml, &["não-deve-existir"]);
expect_wrapped_in(xml, "tagWrapper");

// Fábricas de dados
sample_issuer()        // Emitente Simples Nacional
sample_issuer_normal() // Emitente Regime Normal
sample_recipient()     // Destinatário CPF
sample_item()          // Item com ICMS/PIS/COFINS
sample_payment()       // Pagamento em dinheiro R$20,00
sample_invoice_builder() // NFC-e em homologação

O sample_invoice_builder() retorna um InvoiceBuilder pré-configurado com emitente, item e pagamento no estado Draft, pronto para .build().

Fixtures (tests/fixtures/)

Arquivos de referência copiados das suítes de teste PHP:

DiretórioConteúdo
fixtures/xml/XMLs de NF-e, NFC-e, CT-e, layouts v3.10 e v4.00 (com e sem assinatura, contingência)
fixtures/certs/Certificados PFX de teste (CNPJ e CPF, senha: minhasenha)
fixtures/schemes/Schemas XSD da SEFAZ
fixtures/txt/Arquivos TXT nos layouts legados SPED

O caminho para os fixtures é resolvido em tempo de compilação:

pub const FIXTURES_PATH: &str =
    concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/");

Crates de Teste

O projeto utiliza as seguintes crates de teste:

CrateUso
nextestExecutor de testes paralelo (CI e local)
instaTestes de snapshot
proptestTestes de propriedade
rstestParametrização de testes (fixtures e #[case])
pretty_assertionsDiffs coloridos em falhas de assert_eq!
cargo-fuzzFuzzing com libFuzzer

Executando os Testes

Suíte Completa

# Com nextest (recomendado -- execução paralela)
cargo nextest run --all-features

# Com cargo test padrão
cargo test --all-features

Doc Tests

Doc tests não são capturados pelo nextest, então precisam ser executados separadamente:

cargo test --doc --all-features

Arquivo Individual

# Executar apenas os testes de ICMS
cargo nextest run --all-features -E 'test(tax_icms_test)'

# Executar apenas os snapshot tests
cargo nextest run --all-features -E 'test(snapshot_tests)'

# Executar apenas os property tests
cargo nextest run --all-features -E 'test(property_tests)'

Teste Específico

# Executar um teste pelo nome
cargo nextest run --all-features -E 'test(cst_00_regular_icms)'

# Com cargo test
cargo test --all-features cst_00_regular_icms

Fuzzing

# Instalar cargo-fuzz (requer nightly)
rustup install nightly
cargo install cargo-fuzz

# Listar alvos
cargo +nightly fuzz list

# Executar alvo específico
cargo +nightly fuzz run fuzz_xml_tag_extraction

Padrões de Teste

Testes Portados com XML Inline

Os testes portados frequentemente declaram XML de referência como constantes e verificam comportamento contra ele:

const NFE_REQUEST: &str = r##"<NFe xmlns="...">
    <infNFe versao="4.00" Id="NFe4321...">
        ...
    </infNFe>
</NFe>"##;

#[test]
fn test_to_authorize_nfe_valid() {
    let protocoled = attach_protocol(NFE_REQUEST, RESPONSE)
        .expect("should succeed");
    assert!(protocoled.contains("143220000009921"));
}

Testes Parametrizados com rstest

O rstest permite testar múltiplas variantes do mesmo cenário sem duplicação:

#[rstest]
#[case("40", "<ICMS40>", "<CST>40</CST>")]
#[case("41", "<ICMS40>", "<CST>41</CST>")]
#[case("50", "<ICMS40>", "<CST>50</CST>")]
fn cst_40_41_50_exempt_variants(
    #[case] cst: &str,
    #[case] expected_tag: &str,
    #[case] expected_cst: &str,
) {
    // ... mesmo corpo para as 3 variantes
}

Snapshot Tests com Inline Values

Para valores pequenos e estáveis, o insta suporta asserções inline:

#[test]
fn snapshot_format_cents_1050() {
    assert_snapshot!(format_cents_2(1050), @"10.50");
}

Se o valor mudar, o teste falha e cargo insta review permite revisar e aceitar a mudança.

Property Tests com Invariantes

Os testes de propriedade verificam que invariantes são mantidas para qualquer entrada gerada:

proptest! {
    #[test]
    fn escape_xml_never_contains_raw_special_chars(
        input in "\\PC{1,100}"
    ) {
        let escaped = escape_xml(&input);
        // Nenhum '<', '>', '"' ou '\'' pode aparecer cru
        // '&' só pode aparecer como parte de entidade XML
    }
}

On this page