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:
| Arquivo | Testes | Cobertura |
|---|---|---|
TaxCoverageTest.php | 121 | Todos os branches ICMS CST/CSOSN, FCP, PIS/COFINS |
RenderCoverageTest.php | 26 | NF-e/NFC-e render(), totais, transporte, pagamentos |
CommunicationCoverageTest.php | 75 | Todos os métodos SEFAZ via SoapFake, QRCode, eventos |
TraitsCoverageTest.php | 72 | Todos os traits que estavam com 0% de cobertura |
DeepCoverageTest.php | 76 | Make 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étrica | Valor |
|---|---|
| Testes no fiscal-rs | 739+ |
| Arquivos de teste | 16 |
| Alvos de fuzzing | 5 |
| Testes contribuídos ao sped-nfe | 370 |
| Cobertura sped-nfe (antes → depois) | 40% → 86,5% |
| Fixtures (XML, certificados, schemas, TXT) | 4 diretórios |
Categorias de Teste
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.
| Arquivo | Testes | Origem |
|---|---|---|
make_ported_test.rs | 90 | make-ported.test.ts |
deep_comm_coverage_ported_test.rs | 163 | Suite de cobertura PHP |
tax_coverage_ported_part2_test.rs | 91 | Suite de cobertura PHP |
misc_ported_test.rs | 61 | Utilitários PHP |
tax_coverage_ported_test.rs | 45 | Cálculos tributários PHP |
tools_ported_test.rs | 35 | Tools PHP |
render_coverage_ported_test.rs | 28 | Renderização XML PHP |
complement_ported_test.rs | 4 | Complementos 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). Utilizarstestpara parametrização.tax_pis_cofins_ipi_test.rs(17 testes) -- PIS, COFINS, IPI e Imposto de Importaçãoxml_builder_test.rs(21 testes) -- Construção de XML NF-e/NFC-e com oInvoiceBuildercertificate_test.rs(14 testes) -- Extração de certificados PFX e assinatura digital XMLcomplement_test.rs(8 testes) -- Anexação de protocolo SEFAZ e inutilizaçãoqrcode_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 acceptA 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ódulo | Propriedades verificadas |
|---|---|
| format_utils | Formato monetário sempre tem ponto decimal e N casas; zero produz 0.000...; format_cents_or_zero(None) equivale a zero |
| xml_utils | escape_xml nunca contém caracteres especiais crus; strings seguras passam inalteradas; tag() abre com < e fecha com > |
| tag roundtrip | extract_xml_tag_value(tag(name, text)) retorna o texto original escapado |
| tax_element | Serialização com/sem outer_tag; filter_fields remove Nones; required_field(None) retorna erro |
| state_codes | UFs 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:
| Alvo | O que faz fuzzing |
|---|---|
fuzz_gtin_validation | Validação de códigos GTIN (EAN-8, EAN-13, EAN-14) |
fuzz_response_parsers | Parsers de respostas SOAP da SEFAZ |
fuzz_txt_to_xml | Conversão de layouts TXT legados para XML |
fuzz_xml_standardize | Padronização/normalização de XML |
fuzz_xml_tag_extraction | Extraçã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=60Infraestrutura 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çãoO 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ório | Conteú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:
| Crate | Uso |
|---|---|
| nextest | Executor de testes paralelo (CI e local) |
| insta | Testes de snapshot |
| proptest | Testes de propriedade |
| rstest | Parametrização de testes (fixtures e #[case]) |
| pretty_assertions | Diffs coloridos em falhas de assert_eq! |
| cargo-fuzz | Fuzzing 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-featuresDoc Tests
Doc tests não são capturados pelo nextest, então precisam ser executados separadamente:
cargo test --doc --all-featuresArquivo 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_icmsFuzzing
# 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_extractionPadrõ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
}
}