É bom usar testes de unidade para contar uma história?

13

Então, eu tenho um módulo de autenticação que escrevi há algum tempo. Agora estou vendo os erros do meu caminho e escrevendo testes de unidade para ele. Enquanto escrevia testes de unidade, tenho dificuldade em encontrar bons nomes e boas áreas para testar. Por exemplo, eu tenho coisas como

  • RequerLogin_should_redirect_when_not_logged_in
  • RequerLogin_should_pass_through_when_logged_in
  • Login_should_work_when_given_proper_credentials

Pessoalmente, acho que é um pouco feio, mesmo que pareça "adequado". Também tenho problemas para diferenciar os testes, apenas analisando-os (tenho que ler o nome do método pelo menos duas vezes para saber o que acabou de falhar)

Então, pensei que talvez, em vez de escrever testes que testam puramente a funcionalidade, talvez escreva um conjunto de testes que cubram cenários.

Por exemplo, este é um esboço de teste que eu criei:

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

Isso é ouvido de um padrão? Eu já vi histórias de aceitação e coisas assim, mas isso é fundamentalmente diferente. A grande diferença é que eu estou criando cenários para "forçar" os testes. Em vez de tentar manualmente criar possíveis interações que precisarei testar. Além disso, eu sei que isso incentiva testes de unidade que não testam exatamente um método e classe. Eu acho que isso está bem. Além disso, estou ciente de que isso causará problemas para pelo menos algumas estruturas de teste, pois elas geralmente assumem que os testes são independentes um do outro e a ordem não importa (onde seria neste caso).

Enfim, esse é um padrão aconselhável? Ou isso seria um ajuste perfeito para testes de integração da minha API, e não como testes "unitários"? Isso é apenas em um projeto pessoal, então estou aberto a experimentos que podem ou não correr bem.

Earlz
fonte
4
As linhas entre testes unitários, de integração e funcionais são embaçadas, se eu tivesse que escolher um nome para o seu esboço de teste, seria funcional.
precisa saber é
Eu acho que é uma questão de gosto. Pessoalmente, uso o nome de tudo o que testo em _testanexo e uso comentários para anotar os resultados esperados. Se for um projeto pessoal, encontre algum estilo com o qual se sinta confortável e cumpra-o.
Lister
1
Escrevi uma resposta com detalhes sobre uma maneira mais tradicional de escrever testes de unidade usando o padrão Organizar / Atuar / Afirmar, mas um amigo obteve muito sucesso usando github.com/cucumber/cucumber/wiki/Gherkin , que é usado para especificações e afaik pode gerar testes de pepino.
StuperUser
Enquanto eu não usaria o método que têm demonstrado com nunit ou similar, nspec tem suporte para a criação de contexto e testando em uma história natureza orientada mais: nspec.org
Mike
1
mude "Bill" para "User" e pronto
Steven A. Lowe

Respostas:

15

Sim, é uma boa idéia dar aos seus testes nomes dos cenários de exemplo que você está testando. E usar sua ferramenta de teste de unidade para mais do que apenas testes de unidade, talvez seja bom também, muitas pessoas fazem isso com sucesso (eu também).

Mas não, definitivamente não é uma boa ideia escrever seus testes de uma maneira que importe a ordem de execução dos testes. Por exemplo, o NUnit permite ao usuário selecionar interativamente qual teste ele / ela deseja executar, para que isso não funcione mais da maneira pretendida.

Você pode evitar isso facilmente aqui, separando a parte principal de teste de cada teste (incluindo a "afirmação") das partes que colocam seu sistema no estado inicial correto. Usando seu exemplo acima: escreva métodos para criar uma conta, faça logon e poste um comentário - sem nenhuma afirmação. Em seguida, reutilize esses métodos em diferentes testes. Você também precisará adicionar algum código ao [Setup]método dos equipamentos de teste para garantir que o sistema esteja em um estado inicial definido corretamente (por exemplo, nenhuma conta até o momento no banco de dados, ninguém conectado até o momento etc.).

EDIT: Claro, isso parece contrariar a natureza da "história" dos seus testes, mas se você der nomes significativos aos métodos auxiliares, encontrará suas histórias em cada teste.

Portanto, deve ficar assim:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}
Doc Brown
fonte
Eu diria que usar uma ferramenta xUnit para executar testes funcionais é bom, desde que você não confunda a ferramenta com o tipo de teste e mantenha esses testes separados dos testes de unidade reais, para que os desenvolvedores possam ainda execute testes de unidade rapidamente no momento da confirmação. É provável que sejam muito mais lentos que os testes de unidade.
bdsl
4

Um problema ao contar uma história com testes de unidade é que não torna explícito que os testes de unidade devem ser organizados e executados de forma totalmente independente um do outro.

Um bom teste de unidade deve ser completamente isolado de todos os outros códigos dependentes; é a menor unidade de código que pode ser testada.

Isso oferece o benefício de que, além de confirmar o código, se um teste falhar, você obtém o diagnóstico exatamente onde o código está errado gratuitamente. Se um teste não é isolado, é necessário analisar o que depende para descobrir exatamente o que deu errado e perder um grande benefício do teste de unidade. A ordem de execução também pode gerar muitos falsos negativos, se um teste falhar, é possível que os testes a seguir falhem, apesar do código que eles testam funcionando perfeitamente.

Um bom artigo com mais profundidade é o clássico dos testes híbridos sujos .

Para tornar as classes, métodos e resultados legíveis, o excelente teste Art of Unit usa a convenção de nomenclatura

Classe de teste:

ClassUnderTestTests

Métodos de teste:

MethodUnderTest_Condition_ExpectedResult

Para copiar o exemplo do @Doc Brown, em vez de usar [Setup], que é executado antes de cada teste, escrevo métodos auxiliares para criar objetos isolados para testar.

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

Portanto, os testes com falha têm um nome significativo que fornece uma narrativa sobre exatamente qual método falhou, a condição e o resultado esperado.

É assim que eu sempre escrevi testes de unidade, mas um amigo teve muito sucesso com Gerkin .

StuperUser
fonte
1
Embora eu ache que este é um bom post, eu discordo do que o artigo vinculado diz sobre o teste "híbrido". Ter testes de integração "pequenos" (além disso, não alternativamente aos testes de unidade puros , é claro) pode IMHO ser muito útil, mesmo que eles não possam dizer exatamente qual método contém o código errado. Se esses testes puderem ser mantidos, depende de como o código para esses testes for limpo, eles não serão "sujos" por si só. E acho que o objetivo desses testes pode ser muito claro (como no exemplo do OP).
Doc Brown
3

O que você está descrevendo parece mais com o Behavior Driven Design (BDD) do que o teste de unidade para mim. Dê uma olhada no SpecFlow, que é uma tecnologia .NET BDD baseada no DSL da Gherkin .

Coisas poderosas que qualquer humano pode ler / escrever sem saber nada sobre codificação. Nossa equipe de teste está obtendo grande sucesso aproveitando-a para nossos conjuntos de testes de integração.

Em relação às convenções para testes de unidade, a resposta do @ DocBrown parece sólida.

Drew Marsh
fonte
Para obter informações, o BDD é exatamente como o TDD, é apenas o estilo de escrita que muda. Exemplo: TDD = assert(value === expected)BDD = value.should.equals(expected)+ você descreve os recursos em camadas que resolvem o problema da "independência do teste de unidade". Este é um ótimo estilo!
Offirmo