Vamos supor que eu escrevi um método de extensão em C # para byte
matrizes 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.ToHex
interior do meu projeto, vamos assumir no Foo.Do
mé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.Do
dependerã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?
unit-testing
static-methods
Akira
fonte
fonte
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?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.Respostas:
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.Do
pode 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 zombarTextToHex
. 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.
fonte
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.
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_Works
falhar devido a errosToHex
, todos os testes que dependemToHex
terminarã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 aquiTestes de unidade adicionais que se baseiam
ToHex
também devem ser determinísticos. Se tudoToHex
correr 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.fonte
É um pouco complicado com um exemplo tão minimalista, mas vamos reconsiderar o que estamos fazendo:
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
fonte
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).
fonte
multiply
mé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 é quetoHex
é 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.String
eint
também? Ou classes básicas de utilidade da biblioteca padrão?