Como o teste de unidade funciona?

23

Estou tentando tornar meu código mais robusto e tenho lido sobre testes de unidade, mas acho muito difícil encontrar um uso útil real. Por exemplo, o exemplo da Wikipedia :

public class TestAdder {
    public void testSum() {
        Adder adder = new AdderImpl();
        assert(adder.add(1, 1) == 2);
        assert(adder.add(1, 2) == 3);
        assert(adder.add(2, 2) == 4);
        assert(adder.add(0, 0) == 0);
        assert(adder.add(-1, -2) == -3);
        assert(adder.add(-1, 1) == 0);
        assert(adder.add(1234, 988) == 2222);
    }
}

Eu sinto que esse teste é totalmente inútil, porque você precisa calcular manualmente o resultado desejado e testá-lo, sinto que um teste de unidade melhor aqui seria

assert(adder.add(a, b) == (a+b));

mas isso está apenas codificando a própria função no teste. Alguém pode me fornecer um exemplo em que o teste de unidade é realmente útil? Para sua informação, atualmente estou codificando principalmente funções "procedurais" que recebem ~ 10 booleanos e algumas ints e me dão um resultado int com base nisso. Sinto que o único teste de unidade que eu poderia fazer seria simplesmente re-codificar o algoritmo no teste. edit: eu também deveria ter precisado isso enquanto portava (possivelmente mal projetado) código ruby ​​(que eu não fiz)

lezebulon
fonte
14
How does unit testing work?Ninguém realmente sabe :) #
2928 yannis
30
"você é obrigado a calcular manualmente o resultado desejado". Como isso é "totalmente inútil"? De que outra forma você pode ter certeza de que a resposta está certa?
31511 S.Lott
9
@ S. Lott: É chamado progresso, em tempos antigos as pessoas computadores usados para processar números e economizar tempo, nos dias de hoje as pessoas gastam tempo para fazer computadores com certeza pode processar números: D
Coder
2
@ Codificador: o objetivo do teste de unidade não é "triturar números e economizar tempo";)
Andres F.
7
@lezebulon: o exemplo da Wikipedia não é muito bom, mas isso é um problema com esse caso de teste específico, não com o teste de unidade em geral. Cerca de metade dos dados de teste do exemplo não adiciona nada de novo, tornando-o redundante (eu temo pensar no que o autor desse teste faria com cenários mais complexos). Um teste mais significativo particionaria os dados do teste nos seguintes cenários: "ele pode adicionar números negativos?", "É zero neutro?", "Ele pode adicionar um número negativo e um positivo?".
Andres F.

Respostas:

26

Os testes de unidade, se você estiver testando unidades pequenas o suficiente, sempre estão afirmando o óbvio.

A razão pela qual add(x, y)até é mencionada uma prova unitária é que, algum tempo depois, alguém entrará adde colocará um código especial de manipulação de lógica tributária, sem perceber que a adição é usada em qualquer lugar.

Os testes de unidade são muito sobre o princípio associativo: se A faz B e B faz C, então A faz C. "A faz C" é um teste de nível superior. Por exemplo, considere o seguinte código comercial completamente legítimo:

public void LoginUser (string username, string password) {
    var user = db.FetchUser (username);

    if (user.Password != password)
        throw new Exception ("invalid password");

    var roles = db.FetchRoles (user);

    if (! roles.Contains ("member"))
        throw new Exception ("not a member");

    Session["user"] = user;
}

À primeira vista, isso parece um ótimo método para teste de unidade, porque tem um objetivo muito claro. No entanto, ele faz cerca de 5 coisas diferentes. Cada coisa tem um caso válido e inválido e fará uma enorme permutação de testes de unidade. Idealmente, isso é dividido ainda mais:

public void LoginUser (string username, string password) {

    var user = _userRepo.FetchValidUser (username, password);

    _rolesRepo.CheckUserForRole (user, "member");

    _localStorage.StoreValue ("user", user);
}

Agora estamos reduzidos a unidades. Um teste de unidade não se importa com o que _userRepoconsidera um comportamento válido FetchValidUser, apenas com o nome. Você pode usar outro teste para garantir exatamente o que um usuário válido constitui. Da mesma forma para CheckUserForRole... você separou seu teste de saber como é a estrutura da Função. Você também desvinculou todo o seu programa de ser vinculado estritamente Session. Eu imagino que todas as peças que faltam aqui ficariam assim:

class UserRepository : IUserRepository
{
    public User FetchValidUser (string username, string password)
    {
        var user = db.FetchUser (username);

        if (user.Password != password)
            throw new Exception ("invalid password");

        return user;
    }
}

class RoleRepository : IRoleRepository
{
    public void CheckUserForRole (User user, string role)
    {
        var roles = db.FetchRoles (user);

        if (! roles.Contains (role))
            throw new Exception ("not a member");
    }
}

class SessionStorage : ILocalStorage
{
    public void StoreValue (string key, object value)
    {
        Session[key] = value;
    }
}

Ao refatorar, você realizou várias coisas ao mesmo tempo. O programa é muito mais favorável ao arrancar estruturas subjacentes (você pode abandonar a camada do banco de dados no NoSQL) ou adicionar bloqueios sem interrupções quando perceber que Sessionnão é seguro para threads ou o que quer. Você também já fez testes bastante diretos para escrever para essas três dependências.

Espero que isto ajude :)

Bryan Boettcher
fonte
13

Atualmente, estou codificando principalmente funções "procedurais" que recebem ~ 10 booleanos e algumas ints e me dão um resultado int com base nisso. Sinto que o único teste de unidade que eu poderia fazer seria simplesmente re-codificar o algoritmo no teste

Tenho certeza de que cada uma de suas funções processuais é determinística, portanto, ele retorna um resultado int específico para cada conjunto de valores de entrada. Idealmente, você teria uma especificação funcional a partir da qual poderá descobrir qual resultado deve receber para determinados conjuntos de valores de entrada. Na falta disso, você pode executar o código ruby ​​(que supostamente funciona corretamente) para determinados conjuntos de valores de entrada e registrar os resultados. Em seguida, você precisa HARD CODE os resultados em seu teste. O teste deve ser uma prova de que seu código realmente produz resultados que são conhecidos por estarem corretos .

Mike Nakis
fonte
+1 para executar o código existente e registrar os resultados. Nessa situação, essa é provavelmente a abordagem pragmática.
MarkJ
12

Como ninguém mais parece ter fornecido um exemplo real:

    public void testRoman() {
        RomanNumeral numeral = new RomanNumeral();
        assert( numeral.toRoman(1) == "I" )
        assert( numeral.toRoman(4) == "IV" )
        assert( numeral.toRoman(5) == "V" )
        assert( numeral.toRoman(9) == "IX" )
        assert( numeral.toRoman(10) == "X" )
    }
    public void testSqrt() {
        assert( sqrt(4) == 2 )
        assert( sqrt(9) == 3 )
    }

Você diz:

Eu sinto que esse teste é totalmente inútil, porque você precisa calcular manualmente o resultado desejado e testá-lo

Mas o ponto é que é muito menos provável que você cometa um erro (ou pelo menos mais provável que note seus erros) ao fazer os cálculos manuais e depois ao codificar.

Qual a probabilidade de você cometer um erro no código de conversão decimal em romano? Provavelmente. Qual é a probabilidade de você cometer um erro ao converter números decimais em romanos manualmente? Não muito provável. É por isso que testamos contra cálculos manuais.

Qual a probabilidade de você cometer um erro ao implementar uma função de raiz quadrada? Provavelmente. Qual a probabilidade de você cometer um erro ao calcular manualmente uma raiz quadrada? Provavelmente mais provável. Mas com o sqrt, você pode usar uma calculadora para obter as respostas.

Para sua informação, atualmente estou codificando principalmente funções "procedurais" que recebem ~ 10 booleanos e algumas ints e me dão um resultado int com base nisso. Sinto que o único teste de unidade que eu poderia fazer seria simplesmente re-codificar o algoritmo no teste

Então, eu vou especular sobre o que está acontecendo aqui. Suas funções são meio complicadas, por isso é difícil descobrir pelas entradas qual deve ser a saída. Para fazer isso, você deve executar manualmente (em sua cabeça) a função para descobrir qual é a saída. Compreensivelmente, isso parece meio inútil e propenso a erros.

A chave é que você deseja encontrar as saídas corretas. Mas você precisa testar essas saídas em relação a algo conhecido como correto. Não é bom escrever seu próprio algoritmo para calcular isso, porque isso pode muito bem estar incorreto. Nesse caso, é muito difícil calcular manualmente os valores.

Eu voltaria ao código ruby ​​e executaria essas funções originais com vários parâmetros. Eu pegava os resultados do código ruby ​​e os colocava no teste de unidade. Dessa forma, você não precisa fazer o cálculo manual. Mas você está testando contra o código original. Isso deve ajudar a manter os resultados iguais, mas se houver bugs no original, isso não ajudará. Basicamente, você pode tratar o código original como a calculadora no exemplo sqrt.

Se você mostrou o código real que está transportando, podemos fornecer um feedback mais detalhado sobre como abordar o problema.

Winston Ewert
fonte
E se o código Ruby tiver um bug que você não conhece, que não consta do seu novo código e seu código falhar em um teste de unidade com base nas saídas do Ruby, a investigação sobre por que ele falhou finalmente o justificará e resultará na bug Ruby latente sendo encontrado. Então isso é bem legal.
Adam Wuerl
11

Eu sinto que o único teste de unidade que eu poderia fazer seria simplesmente re-codificar o algoritmo no teste

Você está quase correto para uma classe tão simples.

Experimente para uma calculadora mais complexa. Como uma calculadora de pontuação de boliche.

O valor dos testes de unidade é mais facilmente visto quando você tem regras "comerciais" mais complexas com diferentes cenários para testar.

Não estou dizendo que você não deve testar uma calculadora da série (sua conta calcula problemas em valores como 1/3 que não podem ser representados? O que isso faz com a divisão por zero?), Mas você verá o valorize mais claramente se você testar algo com mais ramos para obter cobertura.

Brian
fonte
4
+1 por notar que se torna mais útil para funções complicadas. E se você decidisse estender adder.add () para valores de ponto flutuante? Matrizes? Valores da conta Leger?
Joshin4colours
6

Apesar do fanatismo religioso cerca de 100% da cobertura do código, direi que nem todo método deve ser testado em unidade. Somente funcionalidade que contém lógica comercial significativa. Uma função que simplesmente adiciona número é inútil para testar.

Atualmente, estou codificando principalmente funções "procedurais" que recebem ~ 10 booleanos e algumas ints e me dão um resultado int com base nisso

Existe o seu problema real ali. Se o teste de unidade parecer estranhamente difícil ou inútil, é provável que seja devido a uma falha no projeto. Se fosse mais orientado a objetos, suas assinaturas de método não seriam tão grandes e haveria menos entradas possíveis para testar.

Eu não preciso entrar no meu OO é superior à programação processual ...

maple_shaft
fonte
Nesse caso, a "assinatura" do método não é massiva; acabei de ler de um std :: vector <bool> que é um membro da classe. Eu também deveria ter precised que eu sou portando (possivelmente mal projetado) código Ruby (que eu não fiz)
lezebulon
2
@lezebulon Independentemente de haver muitas entradas possíveis para esse único método aceitar, esse método está fazendo muito .
maple_shaft
3

No meu ponto de vista, os testes de unidade são úteis até na sua classe de somadores pequenos: não pense em "recodificar" o algoritmo e pense nele como uma caixa preta com o único conhecimento que você tem sobre o comportamento funcional (se você estiver familiarizado com a multiplicação rápida, você conhece algumas tentativas mais rápidas, porém mais complexas, do que usar o "a * b") e sua interface pública. Do que você deve se perguntar "O que diabos pode dar errado?" ...

Na maioria dos casos, isso acontece na borda (vejo que você já teste esses padrões adicionando ++, -, + -, 00 - tempo para completá-los com - +, 0+, 0-, +0, -0). Pense no que acontece em MAX_INT e MIN_INT ao adicionar ou subtrair (adicionar negativos;)) lá. Ou tente garantir que seus testes tenham a aparência exata do que acontece ao redor de zero.

Em suma, o segredo é muito simples (talvez também para os mais complexos;)) para classes simples: pense nos contratos de sua classe (consulte o projeto por contrato) e depois teste contra eles. Quanto melhor você conhecer suas inv, pre e post, mais "completas" serão seus testes.

Dica para suas classes de teste: tente escrever apenas uma afirmação em um método. Dê bons nomes aos métodos (por exemplo, "testAddingToMaxInt", "testAddingTwoNegatives") para obter o melhor feedback quando o teste falhar após a alteração do código.

Sebastian Bauer
fonte
2

Em vez de testar um valor de retorno calculado manualmente ou duplicar a lógica no teste para calcular o valor de retorno esperado, teste o valor de retorno para uma propriedade esperada.

Por exemplo, se você deseja testar um método que inverte uma matriz, não deseja inverter manualmente seu valor de entrada, multiplique o valor de retorno pela entrada e verifique se obtém a matriz de identidade.

Para aplicar essa abordagem ao seu método, você precisará considerar sua finalidade e semântica, para identificar quais propriedades o valor de retorno terá em relação às entradas.

JGWeissman
fonte
2

Os testes de unidade são uma ferramenta de produtividade. Você recebe uma solicitação de alteração, implementa-a e, em seguida, executa seu código no gambit de teste de unidade. Esse teste automatizado economiza tempo.

I feel that this test is totally useless, because you are required to manually compute the wanted result and test it, I feel like a better unit test here would be

Um ponto discutível. O teste no exemplo mostra apenas como instanciar uma classe e executá-la através de uma série de testes. Concentrar-se nas minúcias de uma única implementação está faltando a floresta para as árvores.

Can someone provide me with an example where unit testing is actually useful?

Você tem uma entidade de funcionário. A entidade contém um nome e endereço. O cliente decide adicionar um campo ReportsTo.

void TestBusinessLayer()
{
   int employeeID = 1234
   Employee employee = Employee.GetEmployee(employeeID)
   BusinessLayer bl = new BusinessLayer()
   Assert.isTrue(bl.Add(employee))//assume Add returns true on pass
}

Esse é um teste básico do BL para trabalhar com um funcionário. O código passará / falhará na alteração do esquema que você acabou de fazer. Lembre-se de que afirmações não são a única coisa que o teste faz. A execução do código também garante que não haja exceções.

Com o tempo, a realização dos testes facilita as alterações em geral. O código é testado automaticamente em busca de exceções e em relação às asserções feitas. Isso evita grande parte das despesas gerais incorridas pelos testes manuais de um grupo de controle de qualidade. Embora a UI ainda seja bastante difícil de automatizar, as outras camadas geralmente são muito fáceis, desde que você use os modificadores de acesso corretamente.

I feel like the only unit testing I could do would be to simply re-code the algorithm in the test.

Até a lógica processual é facilmente encapsulada dentro de uma função. Encapsular, instanciar e passar o int / primitivo a ser testado (ou objeto de simulação). Não copie e cole o código em um teste de unidade. Isso derrota o DRY. Ele também derrota o teste inteiramente porque você não está testando o código, mas uma cópia do código. Se o código que deveria ter sido testado mudar, o teste ainda passará!

P.Brian.Mackey
fonte
<pedantry> "gama", não "gambit". </
pedantry
@chao lol aprender algo novo todos os dias.
usar o seguinte
2

Tomando o seu exemplo (com um pouco de refatoração),

assert(a + b, math.add(a, b));

não ajuda a:

  • entender como math.addse comporta internamente,
  • saiba o que acontecerá com casos extremos.

É como dizer:

  • Se você quiser saber o que o método faz, consulte as centenas de linhas de código-fonte (porque, sim, math.add pode conter centenas de LOC; veja abaixo).
  • Não me preocupo em saber se o método funciona corretamente. Tudo bem se os valores esperados e reais forem diferentes do que eu realmente esperava .

Isso também significa que você não precisa adicionar testes como:

assert(3, math.add(1, 2));
assert(4, math.add(2, 2));

Eles também não ajudam, ou pelo menos, uma vez que você fez a primeira afirmação, a segunda não traz nada útil.

Em vez disso, o que dizer de:

const numeric Pi = 3.1415926535897932384626433832795;
const numeric Expected = 4.1415926535897932384626433832795;
assert(Expected, math.add(Pi, 1),
    "Adding an integer to a long numeric doesn't give a long numeric result.");
assert(Expected, math.add(1, Pi),
    "Adding a long numeric to an integer doesn't give a long numeric result.");

Isso é auto-explicativo e muito útil para você e para a pessoa que manterá o código-fonte posteriormente. Imagine que essa pessoa faça uma ligeira modificação no math.addpara simplificar o código e otimizar o desempenho e veja o resultado do teste como:

Test TestNumeric() failed on assertion 2, line 5: Adding a long numeric to an
integer doesn't give a long numeric result.

Expected value: 4.1415926535897932384626433832795
Actual value: 4

essa pessoa entenderá imediatamente que o método recém-modificado depende da ordem dos argumentos: se o primeiro argumento for um número inteiro e o segundo for um numérico longo, o resultado seria um inteiro, enquanto um numérico longo é esperado.

Da mesma forma, obter o valor real de 4.141592na primeira asserção é auto-explicativo: você sabe que o método deve lidar com uma grande precisão , mas, na verdade, falha.

Pela mesma razão, duas afirmações a seguir podem fazer sentido em alguns idiomas:

// We don't expect a concatenation. `math` library is not intended for this.
assert(0, math.add("Hello", "World"));

// We expect the method to convert every string as if it was a decimal.
assert(5, math.add("0x2F", 5));

Além disso, o que dizer de:

assert(numeric.Infinity, math.add(numeric.Infinity, 1));

Também é auto-explicativo: você deseja que seu método seja capaz de lidar corretamente com o infinito. Ir além do infinito ou lançar uma exceção não é um comportamento esperado.

Ou talvez, dependendo do seu idioma, isso faça mais sentido?

/**
 * Ensures that when adding numbers which exceed the maximum value, the method
 * fails with OverflowException, instead of restarting at numeric.Minimum + 1.
 */
TestOverflow()
{
    UnitTest.ExpectException(ofType(OverflowException));

    numeric result = math.add(numeric.Maximum, 1));

    UnitTest.Fail("The tested code succeeded, while an OverflowException was
        expected.");
}
Arseni Mourzenko
fonte
1

Para uma função muito simples como add, o teste pode ser considerado desnecessário, mas à medida que suas funções se tornam mais complexas, fica cada vez mais óbvio o motivo pelo qual o teste é necessário.

Pense no que você faz quando está programando (sem teste de unidade). Geralmente você escreve algum código, executa, vê que funciona e passa para a próxima coisa, certo? À medida que você escreve mais código, especialmente em um sistema / GUI / site muito grande, você descobre que precisa fazer mais e mais "correr e ver se funciona". Você tem que tentar isso e tentar aquilo. Depois, faça algumas alterações e tente novamente as mesmas coisas. Torna-se muito óbvio que você poderia economizar tempo escrevendo testes de unidade que automatizariam toda a parte "executando e ver se funciona".

À medida que seus projetos se tornam cada vez maiores, o número de coisas que você precisa "executar e ver se funciona" torna-se irreal. Então você acaba executando e tentando alguns dos principais componentes da GUI / projeto e depois esperando que tudo esteja bem. Isto é uma receita para o desastre. É claro que você, como ser humano, não pode testar repetidamente todas as situações possíveis que seus clientes possam usar se a GUI for usada por literalmente centenas de pessoas. Se você tivesse testes de unidade, poderia simplesmente executar o teste antes de enviar a versão estável ou mesmo antes de se comprometer com o repositório central (se o seu local de trabalho usar um). E, se houver algum erro encontrado posteriormente, você pode adicionar um teste de unidade para verificar no futuro.

Asaf
fonte
1

Um dos benefícios de escrever testes de unidade é que ele ajuda a escrever código mais robusto, forçando-o a pensar em casos extremos. Que tal testar alguns casos extremos, como excesso de número inteiro, truncamento decimal ou manipulação de nulos para os parâmetros?

Penhasco
fonte
0

Talvez você assuma que add () foi implementado com a instrução ADD. Se algum programador júnior ou engenheiro de hardware reimplementar a função add () usando ANDS / ORS / XORS, bit inverte e alterna, convém testá-lo de acordo com a instrução ADD.

Em geral, se você substituir as tripas de add (), ou a unidade em teste, por um número aleatório ou gerador de saída, como você saberia que algo estava quebrado? Codifique esse conhecimento em seus testes de unidade. Se ninguém souber se está quebrado, basta verificar o código de rand () e voltar para casa, seu trabalho está feito.

hotpaw2
fonte
0

Eu posso ter perdido isso entre todas as respostas, mas, para mim, a principal motivação por trás do Teste de Unidade é menos provar a correção de um método hoje, mas provar a correção contínua desse método sempre que você o altera .

Tome uma função simples, como retornar o número de itens em alguma coleção. Hoje, quando sua lista é baseada em uma estrutura de dados interna que você conhece bem, você pode pensar que esse método é tão dolorosamente óbvio que você não precisa de um teste para isso. Em alguns meses ou anos, você (ou outra pessoa ) decide [s] substituir a estrutura interna da lista. Você ainda precisa saber que getCount () retorna o valor correto.

É aí que seus testes de unidade realmente se destacam.

Você pode alterar a implementação interna do seu código, mas para qualquer consumidor desse código, o resultado permanece o mesmo.

Phill W.
fonte