Eu realmente preciso de uma estrutura de teste de unidade?

19

Atualmente no meu trabalho, temos um grande conjunto de testes de unidade para nosso aplicativo C ++. No entanto, não usamos uma estrutura de teste de unidade. Eles simplesmente utilizam uma macro C que basicamente envolve uma afirmação e um cout. Algo como:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

Então, simplesmente criamos funções para cada um de nossos testes, como

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

Nosso servidor de IC captura "falha no teste de unidade" e falha na compilação, enviando a mensagem por e-mail aos desenvolvedores.

E se tivermos código de instalação duplicado, simplesmente o refatoramos como qualquer outro código duplicado que teríamos em produção. Nós o envolvemos por trás das funções auxiliares, fazemos algumas classes de teste agruparem a configuração de cenários usados ​​com freqüência.

Eu sei que existem estruturas por aí como o CppUnit e o teste de unidade de impulso. Gostaria de saber qual o valor que estes adicionam? Estou sentindo falta do que isso traz para a mesa? Existe algo útil que eu possa ganhar com eles? Estou hesitante em adicionar uma dependência, a menos que agregue valor real, especialmente porque parece que o que temos é simples e funciona bem.

Doug T.
fonte

Respostas:

8

Como outros já disseram, você já possui sua própria estrutura simples e caseira.

Parece ser trivial fazer um. No entanto, existem alguns outros recursos de uma estrutura de teste de unidade que não são tão fáceis de implementar, porque exigem algum conhecimento avançado da linguagem. Os recursos que geralmente exigem de uma estrutura de teste e não são tão fáceis de homebrew são:

  • Coleta automática de casos de teste. Ou seja, definir o novo método de teste deve ser suficiente para executá-lo. O JUnit coleta automaticamente todos os métodos cujos nomes começam com test, NUnit possui a [Test]anotação, Boost.Test usa as macros BOOST_AUTO_TEST_CASEe BOOST_FIXTURE_TEST_CASE.

    É principalmente conveniência, mas toda a conveniência que você pode obter aumenta a chance de os desenvolvedores escreverem os testes que deveriam e de que os conectarão corretamente. Se você tiver instruções longas, alguém perderá parte delas agora e, talvez, alguns testes não estejam sendo executados e ninguém notará.

  • Capacidade de executar casos de teste selecionados, sem alterar o código e recompilar. Qualquer estrutura decente de teste de unidade permite especificar quais testes você deseja executar na linha de comando. Se você deseja depurar em testes de unidade (é o ponto mais importante para muitos desenvolvedores), é necessário selecionar apenas alguns para executar, sem alterar o código em todo o lugar.

    Digamos que você acabou de receber o relatório de bug # 4211 e pode ser reproduzido com teste de unidade. Então você escreve um, mas precisa dizer ao corredor para executar exatamente esse teste, para poder depurar o que está realmente errado lá.

  • Capacidade de marcar as falhas esperadas dos testes, por caso de teste, sem modificar as próprias verificações. Na verdade, trocamos as estruturas no trabalho para obter essa.

    Qualquer suíte de teste de tamanho decente terá testes que estão falhando porque os recursos testados ainda não foram implementados, ainda não foram concluídos, ninguém teve tempo para corrigi-los ou algo assim. Sem a capacidade de marcar testes como falhas esperadas, você não notará outra falha quando houver algumas regularmente; portanto, os testes param de servir ao seu principal objetivo.

Jan Hudec
fonte
obrigado Eu acho que esta é a melhor resposta. No momento, minha macro faz seu trabalho, mas não posso fazer nenhum dos recursos mencionados.
Doug T.
1
@Jan Hudec "É principalmente conveniência, mas toda a conveniência que você pode obter aumenta a chance de os desenvolvedores realmente escreverem os testes que deveriam e de que os conectarão corretamente."; Todas as estruturas de teste são (1) não triviais para instalação, geralmente têm mais instruções de instalação desatualizadas ou não exaustivas do que as instruções válidas atualizadas; (2) se você se comprometer diretamente com uma estrutura de teste, sem uma interface no meio, estiver casado com ela, alternar estruturas nem sempre é fácil.
Dmitry
@ Jan Hudec Se esperamos que mais pessoas escrevam testes de unidade, precisamos ter mais resultados no google para "O que é um teste de unidade", do que "O que é teste de unidade". Não faz sentido realizar testes de unidade se você não tem idéia do que é um teste de unidade independente de qualquer estrutura de teste de unidade ou definição de teste de unidade. Você não pode fazer o teste de unidade, a menos que tenha uma forte compreensão do que é um teste de unidade, caso contrário, não há sentido em fazer o teste de unidade.
Dmitry
Eu não compro esse argumento de conveniência. Escrever código de teste é muito difícil se você deixar o mundo trivial dos exemplos. Todos esses modelos, configurações, bibliotecas, programas de servidor de modelos externos etc. Todos eles exigem que você conheça a estrutura de teste de dentro para fora.
Lothar
@ Lothar, sim, é muito trabalho e muito a aprender, mas ainda é preciso escrever um simples clichê repetidas vezes, porque faltam alguns utilitários úteis que tornam o trabalho muito menos agradável e isso faz uma diferença notável na efetividade.
Jan Hudec
27

Parece que você já usa uma estrutura caseira.

Qual é o valor agregado de estruturas mais populares? Eu diria que o valor que eles agregam é que, quando você tem que trocar código com pessoas de fora da sua empresa, você pode fazê-lo, pois ele se baseia na estrutura conhecida e amplamente usada .

Por outro lado, uma estrutura caseira obriga você a nunca compartilhar seu código ou a fornecer a própria estrutura, o que pode se tornar complicado com o crescimento da própria estrutura.

Se você der seu código a um colega como está, sem explicação e sem estrutura de teste de unidade, ele não poderá compilá-lo.

Uma segunda desvantagem das estruturas caseiras é a compatibilidade . Estruturas populares de teste de unidade tendem a garantir a compatibilidade com diferentes IDEs, sistemas de controle de versão etc. Por enquanto, pode não ser muito importante para você, mas o que acontecerá se um dia você precisar alterar algo no servidor de CI ou migrar para um novo IDE ou um novo VCS? Você vai reinventar a roda?

Por último, mas não menos importante, estruturas maiores fornecem mais recursos que você pode implementar em sua própria estrutura um dia. Assert.AreEqual(expected, actual)nem sempre é suficiente. E se você precisar:

  • medir precisão?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • teste nulo se for executado por muito tempo? Reimplementar um tempo limite pode não ser simples, mesmo em idiomas que facilitam a programação assíncrona.

  • testar um método que espera que uma exceção seja lançada?

  • tem um código mais elegante?

    Assert.Verify(a == null);
    

    está bem, mas não é mais expressivo sua intenção de escrever a próxima linha?

    Assert.IsNull(a);
    
Arseni Mourzenko
fonte
O "framework" que usamos é todo em um arquivo de cabeçalho muito pequeno e segue a semântica de assert. Portanto, não estou muito preocupado com os inconvenientes que você lista.
Doug T.
4
Considero as afirmações a parte mais trivial da estrutura de teste. O corredor que coleta e executa os casos de teste e verifica os resultados é a parte importante não trivial.
Jan Hudec
@ Jan Eu não sigo muito. Meu corredor é uma rotina principal comum a todos os programas C ++. Um corredor de estrutura de teste de unidade faz algo mais sofisticado e útil?
Doug T.
1
Sua estrutura permite apenas a semântica de assert e executar testes em um método principal ... até agora. Basta esperar até que você tem que grupo o seu afirma em vários cenários, cenários de grupo relacionadas juntos com base em dados inicializados, etc.
James Kingsbery
@DougT .: Sim, o executor de estrutura de teste de unidade decente faz algumas coisas úteis mais sofisticadas. Veja minha resposta completa.
Jan Hudec
4

Como outros já disseram, você já possui sua própria estrutura caseira.

A única razão que vejo para usar alguma outra estrutura de teste seria do ponto de vista do "conhecimento comum" do setor. Os novos desenvolvedores não precisariam aprender do seu jeito caseiro (embora pareça muito simples).

Além disso, outras estruturas de teste podem ter mais recursos dos quais você poderia aproveitar.

ozz
fonte
1
Acordado. Se você não está enfrentando limitações com sua estratégia de teste atual, vejo poucas razões para mudar. Uma boa estrutura provavelmente forneceria melhores recursos de organização e geração de relatórios, mas você precisaria justificar o trabalho adicional necessário para integrar-se à sua base de código (incluindo seu sistema de compilação).
TMN
3

Você já possui uma estrutura, mesmo que simples.

As principais vantagens de uma estrutura maior, como as vejo, são a capacidade de ter muitos tipos diferentes de asserções (como afirmar aumentos), uma ordem lógica para os testes de unidade e a capacidade de executar apenas um subconjunto de testes de unidade em um tempo. Além disso, o padrão dos testes xUnit é bastante agradável de seguir, se você puder - por exemplo, o setUP () e tearDown (). Obviamente, isso trava você no referido quadro. Observe que algumas estruturas têm melhor integração simulada do que outras - google mock e test, por exemplo.

Quanto tempo você leva para refatorar todos os seus testes de unidade para uma nova estrutura? Dias ou algumas semanas talvez valham a pena, mas mais talvez não tanto.

Sardathrion - Restabelecer Monica
fonte
2

Na minha opinião, vocês dois têm a vantagem e estão em "desvantagem" (sic).

A vantagem é que você tem um sistema com o qual se sente confortável e que funciona para você. Você está feliz por confirmar a validade do seu produto e provavelmente não encontraria valor comercial ao tentar alterar todos os seus testes por algo que usa uma estrutura diferente. Se você pode refatorar seu código e seus testes detectarem as alterações - ou, melhor ainda, se você puder modificar seus testes e seu código existente falhar nos testes até que seja refatorado, você terá todas as suas bases cobertas. Contudo...

Uma das vantagens de ter uma API de teste de unidade bem projetada é que há muito suporte nativo na maioria dos IDEs modernos. Isso não afetará os usuários avançados do VI e do emacs que zombam dos usuários do Visual Studio, mas para aqueles que usam um bom IDE, você pode depurar seus testes e executá-los dentro o próprio IDE. Isso é bom, no entanto, há uma vantagem ainda maior, dependendo da estrutura que você usa, e está no idioma usado para testar seu código.

Quando digo linguagem , não estou falando de uma linguagem de programação, mas de um conjunto rico de palavras envolvidas em uma sintaxe fluente que faz com que o código de teste seja lido como uma história. Em particular, me tornei um defensor do uso de estruturas de BDD . Minha API DotNet BDD favorita é StoryQ, mas existem vários outros com o mesmo objetivo básico, que é extrair um conceito de um documento de requisitos e escrevê-lo no código de maneira semelhante à maneira como ele é escrito na especificação. As APIs realmente boas, no entanto, vão ainda mais longe, interceptando todas as instruções individuais de um teste e indicando se essa instrução foi executada com êxito ou falhou. Isso é incrivelmente útil, pois você vê todo o teste executado sem retornar mais cedo, o que significa que seus esforços de depuração se tornam incrivelmente eficientes, pois você só precisa focar sua atenção nas partes do teste que falharam, sem precisar decodificar a chamada inteira seqüência. A outra coisa interessante é que a saída do teste mostra todas essas informações,

Como um exemplo do que estou falando, compare o seguinte:

Usando declarações:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

Usando uma API BDD fluente: (Imagine que os bits em itálico são basicamente indicadores de método)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

Agora que a sintaxe do BDD é mais longa e mais elaborada, e esses exemplos são terrivelmente inventados, no entanto, para situações de teste muito complexas nas quais muitas coisas estão mudando em um sistema como resultado de um determinado comportamento do sistema, a sintaxe do BDD oferece uma clara descrição sobre o que você está testando e como sua configuração de teste foi definida. Você pode mostrar esse código a um não programador e ele entenderá instantaneamente o que está acontecendo. Além disso, se "variable_A" falhar no teste em ambos os casos, o exemplo Asserts não será executado após a primeira afirmação até que você tenha corrigido o problema, enquanto a API BDD executará todos os métodos chamados na cadeia e indicará quais partes individuais da declaração estavam erradas.

Pessoalmente, acho que essa abordagem funciona muito melhor do que as estruturas xUnit mais tradicionais, no sentido de que a linguagem de teste é a mesma que seus clientes falarão de seus requisitos lógicos. Mesmo assim, eu consegui usar as estruturas xUnit de um estilo semelhante sem precisar inventar uma API de teste completa para apoiar meus esforços e, embora as declarações ainda efetivamente entrem em curto-circuito, elas são lidas com mais clareza. Por exemplo:

Usando Nunit :

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

Se você decidir explorar o uso de uma API de teste de unidade, meu conselho é experimentar um grande número de APIs diferentes por um tempo e manter a mente aberta sobre sua abordagem. Embora eu defenda pessoalmente o BDD, suas necessidades comerciais podem exigir algo diferente para as circunstâncias da sua equipe. A chave, no entanto, é evitar adivinhar o sistema existente. Você sempre pode oferecer suporte aos testes existentes com alguns testes usando outra API, se necessário, mas eu certamente não recomendaria uma enorme reescrita de teste apenas para tornar tudo igual. Como o código legado fica fora de uso, você pode facilmente substituí-lo e seus testes por um novo código, além de testar usando uma API alternativa, e isso sem a necessidade de investir em um grande esforço que não trará necessariamente nenhum valor comercial real. Quanto ao uso de uma API de teste de unidade,

S.Robins
fonte
1

O que você tem é simples e faz o trabalho. Se isso funciona para você, ótimo. Você não precisa de uma estrutura de teste de unidade convencional, e eu hesitaria em passar para o trabalho de portar uma biblioteca existente de testes de unidade para uma nova estrutura. Eu acho que o maior valor das estruturas de teste de unidade é reduzir a barreira à entrada; você apenas começa a escrever testes, porque a estrutura já está em vigor. Você já passou desse ponto e não obterá esse benefício.

O outro benefício de usar uma estrutura convencional - e é um benefício menor, a IMO - é que os novos desenvolvedores já podem estar atualizados com qualquer estrutura que você esteja usando e, portanto, exigirá menos treinamento. Na prática, com uma abordagem direta como a que você descreveu, isso não deve ser um grande problema.

Além disso, a maioria das estruturas mainstream possui certos recursos que sua estrutura pode ou não ter. Esses recursos reduzem o código do encanamento e tornam mais rápido e fácil a gravação de casos de teste:

  • Execução automática de casos de teste, usando convenções de nomenclatura, anotações / atributos, etc.
  • Várias afirmações mais específicas, para que você não precise escrever lógica condicional para todas as suas afirmações ou capturar exceções para afirmar seu tipo.
  • Categorização de casos de teste, para que você possa executar facilmente subconjuntos deles.
Adam Jaskiewicz
fonte