Como posso testar métodos de unidade que estão usando métodos estáticos?


8

Vamos supor que eu escrevi um método de extensão em C # para bytematrizes que os codifica em cadeias hexadecimais, da seguinte maneira:

public static class Extensions
{
    public static string ToHex(this byte[] binary)
    {
        const string chars = "0123456789abcdef";
        var resultBuilder = new StringBuilder();
        foreach(var b in binary)
        {
            resultBuilder.Append(chars[(b >> 4) & 0xf]).Append(chars[b & 0xf]);
        }
        return resultBuilder.ToString();
    }
}

Eu poderia testar o método acima usando o NUnit da seguinte maneira:

[Test]
public void TestToHex_Works()
{
    var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
    Assert.AreEqual("0123456789abcdef", bytes.ToHex());
}

Se eu usar o Extensions.ToHexinterior do meu projeto, vamos assumir no Foo.Dométodo da seguinte maneira:

public class Foo
{
    public bool Do(byte[] payload)
    {
        var data = "ES=" + payload.ToHex() + "ff";
        // ...
        return data.Length > 5;
    }
    // ...
}

Então todos os testes de Foo.Dodependerão do sucesso de TestToHex_Works.

Usando funções livres em C ++, o resultado será o mesmo: testes que testam métodos que usam funções livres dependerão do sucesso de testes de funções livres.

Como posso lidar com essas situações? De alguma forma, posso resolver essas dependências de teste? Existe uma maneira melhor de testar os trechos de código acima?


4
Then all tests of Foo.Do will depend on the success of TestToHex_works-- Assim? Você não tem aulas que dependem do sucesso de outras aulas?
Robert Harvey

5
Eu nunca entendi muito bem essa obsessão pelas funções estáticas / livres e sua chamada não-testabilidade. Se uma função livre está livre de efeitos colaterais, é a coisa mais fácil do planeta para testar e provar que funciona. Você demonstrou isso de maneira bastante eficaz em sua própria pergunta. Como você testa métodos comuns e livres de efeitos colaterais (que não dependem do estado da classe) em instâncias de objetos? Eu sei que você tem alguns desses.
Robert Harvey

2
A única desvantagem desse código usando funções estáticas é que você não pode usar facilmente algo além de toHex(ou trocar implementações). Além disso, está tudo bem. Seu código convertido em hexadecimal foi testado, agora há outro código usando esse código testado como um utilitário para atingir seu próprio objetivo.
Steve Chamaillard

3
Estou completamente perdendo qual é o problema aqui. Se o ToHex não funcionar, é claro que o Do também não funcionará.
Simon B

5
O teste para Foo.Do () não deve saber ou se importar com o nome de ToHex (), é um detalhe de implementação.
17 de 26

Respostas:


37

Então todos os testes de Foo.Dodependerão do sucesso do TestToHex_Works.

Sim. É por isso que você tem testes TextToHex. Se esses testes forem aprovados, a função atenderá às especificações definidas nesses testes. Então, Foo.Dopode chamá-lo com segurança e não se preocupar com isso. Já está coberto.

Você pode adicionar uma interface, transformar o método em um método de instância e injetá-lo Foo. Então você pode zombar TextToHex. Mas agora você precisa escrever uma simulação, que pode funcionar de maneira diferente. Portanto, você precisará de um teste de "integração" para reunir os dois e garantir que as peças realmente funcionem juntas. O que isso conseguiu além de tornar as coisas mais complexas?

A idéia de que os testes de unidade devem testar partes do seu código isoladamente de outras partes é uma falácia. A "unidade" em um teste de unidade é uma unidade de execução isolada. Se dois testes puderem ser executados simultaneamente sem afetar um ao outro, eles serão executados isoladamente e os testes de unidade também. As funções estáticas que são rápidas, não têm uma configuração complexa e não têm efeitos colaterais, como o seu exemplo, portanto, podem ser usadas diretamente em testes de unidade. Se você possui um código lento, complexo de configurar ou com efeitos colaterais, as zombarias são úteis. Eles devem ser evitados em outros lugares embora.


2
Você faz um bom argumento para o desenvolvimento "teste primeiro". A zombaria existe em parte porque o código é gravado de tal maneira que é muito difícil de testar e, se você escreve seus testes primeiro, se força a escrever um código que é mais facilmente testável.
Robert Harvey

3
Estou votando apenas por recomendar não zombar de funções puras. Visto isso muitas vezes.
Jared Smith

2

Usando funções livres em C ++, o resultado será o mesmo: testes que testam métodos que usam funções livres dependerão do sucesso de testes de funções livres.

Como posso lidar com essas situações? De alguma forma, posso resolver essas dependências de teste? Existe uma maneira melhor de testar os trechos de código acima?

Bem, não vejo dependência aqui. Pelo menos não do tipo que nos obriga a executar um teste antes do outro. A dependência que construímos entre os testes (não importa o tipo) é uma confiança .

Criamos um pedaço de código (teste primeiro ou não) e garantimos que os testes sejam aprovados. Então, estamos em posição de construir mais código. Todo o código construído sobre esse primeiro é construído sobre confiança e certeza. Isso é mais ou menos o que @DavidArno explica (muito bem) em sua resposta.

Sim. É por isso que você tem testes para o X. Se esses testes forem aprovados, a função atenderá às especificações definidas nesses testes. Assim, você pode chamá-lo com segurança e não se preocupar com isso. Já está coberto.

Os testes de unidade devem ser executados em qualquer ordem, a qualquer hora, em qualquer ambiente e o mais rápido possível. Se TestToHex_Worksé executado o primeiro ou o último não deve preocupar você.

Se TestToHex_Worksfalhar devido a erros ToHex, todos os testes que dependem ToHexterminarão com resultados diferentes e falharão (idealmente). A chave aqui é detectar esses resultados diferentes. Fazemos isso fazendo testes de unidade para serem determinísticos. Como você faz aqui

var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
Assert.AreEqual("0123456789abcdef", bytes.ToHex());

Testes de unidade adicionais que se baseiam ToHextambém devem ser determinísticos. Se tudo ToHexcorrer bem, o resultado deve ser o esperado. Se você pegar uma diferente, algo deu errado, em algum lugar e é isso que você deseja em um teste de unidade, para detectar essas mudanças sutis e falhar rapidamente.


1

É um pouco complicado com um exemplo tão minimalista, mas vamos reconsiderar o que estamos fazendo:

Vamos supor que eu escrevi um método de extensão em c # para matrizes de bytes que as codifique em cadeias hexadecimais, da seguinte maneira:

Por que você escreveu um método de extensão para fazer isso? A menos que você esteja escrevendo uma biblioteca para codificar matrizes de bytes em seqüências de caracteres, esse provavelmente não é o requisito que você está tentando cumprir. O que parece que você estava fazendo aqui estava tentando atender ao requisito "Validar alguma carga útil que é uma matriz de bytes" - portanto, os testes de unidade que você está implementando devem ser "Dada uma carga útil X válida, meu método retorna verdadeiro" e "Fornecido uma carga útil Y inválida, meu método retorna false ".

Portanto, quando você implementa isso inicialmente, você pode fazer o material "byte-to-hex" embutido no método. E tudo bem. Posteriormente, você obtém algum outro requisito (por exemplo, "Exibir a carga útil como uma sequência hexadecimal") que, enquanto você a implementa, você percebe que também exige que você converta uma matriz de bytes em uma sequência hexadecimal. Nesse ponto, você cria seu método de extensão, refatora o método antigo e o chama no seu novo código. Seus testes para a funcionalidade de validação não devem mudar. Seus testes para exibir a carga útil devem ser os mesmos, independentemente de o código de bytes em hexadecimal estar embutido ou em um método estático. O método estático é um detalhe de implementação com o qual você não deve se preocupar ao escrever os testes. O código no método estático será testado por seus consumidores. Eu não


"esse provavelmente não é o requisito que você está tentando cumprir. O que parece que você estava fazendo aqui foi tentar cumpri-lo" - você está fazendo muitas suposições sobre o objetivo de uma função que provavelmente foi escolhido apenas como um exemplo representativo, e não como resultado de um programa maior.
whatsisname 27/06/19

A "unidade" em "teste de unidade" não significa uma única função ou classe. Significa uma unidade de funcionalidade. A menos que você esteja escrevendo especificamente uma biblioteca que converteu bytes em strings, essa parte é um detalhe de implementação de um pouco mais de comportamento útil. Esse comportamento útil é a parte para a qual você deseja realizar os testes, porque é com isso que você se importa em estar correto. Em um mundo ideal, você deseja encontrar uma biblioteca que já existe para fazer o bin-to-hex (ou seja o que for, não se preocupe com os detalhes), troque-a pela sua implementação e não altere nenhuma testes.
Chris Cooper

O diabo está nos detalhes. O teste de unidade "detalhes de implementação" ainda é uma coisa extremamente útil a ser feita e não é algo a ser analisado.
Whatsisname

0

Exatamente. E este é um dos problemas com métodos estáticos, outro é que o POO é uma alternativa muito melhor na maioria das situações.

Esse também é um dos motivos pelos quais a injeção de dependência é usada.

No seu caso, você pode preferir ter um conversor concreto injetado em uma classe que precise da conversão de alguns valores em hexadecimal. Uma interface , implementada por essa classe concreta, definiria o contrato necessário para converter os valores.

Você não apenas seria capaz de testar seu código com mais facilidade, como também poderia trocar implementações mais tarde (já que existem muitas outras maneiras possíveis de converter valores em hexadecimal, algumas delas produzindo saídas diferentes).


2
Como isso não tornará o código dependente do sucesso dos testes do conversor? Injetar uma classe, uma interface ou qualquer coisa que você queira não a faz funcionar magicamente. Se você estivesse falando sobre efeitos colaterais, sim, haveria muito valor injetando uma implementação falsa, em vez de codificada, mas esse problema nunca existiu em primeiro lugar.
Steve Chamaillard

1
@ArseniMourzenko, então, ao injetar uma classe de calculadora implementando um multiplymétodo, você zombaria disso? O que significa que, para refatorar (e usar o operador *), você precisará alterar todas as declarações simuladas do seu código? Não parece um código fácil para mim. Tudo o que estou dizendo é que toHexé muito simples e não tem efeitos colaterais, por isso não vejo motivo para zombar ou fazer isso. É muito caro para absolutamente nenhum lucro. Não estou dizendo que não devemos injetar, mas isso depende do uso.
Steve Chamaillard

2
@ Ewan, se eu modificasse um pedaço de código e depois enfrentasse " um mar de vermelho ", poderia pensar que não, por onde começar? Mas talvez eu pudesse voltar ao código que acabei de modificar e ver os testes primeiro para ver o que quebrei. : p
David Arno

3
@ArseniMourzenko, se o DI torna seu design muito melhor, por que não vejo argumentos para injetar tipos como Stringe inttambém? Ou classes básicas de utilidade da biblioteca padrão?
Bart van Ingen Schenau

4
Sua resposta ignora completamente aspectos práticos. Existem muitas situações com métodos estáticos que são rápidos, independentes e improváveis ​​o suficiente para mudar, que ir ao esforço de injetá-las ou pular qualquer aro que não seja uma chamada de função regular é um desperdício inútil de tempo e esforço.
whatsisname 27/06/19
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.