Como exatamente os testes unitários devem ser escritos sem zombar extensivamente?

64

Pelo que entendi, o ponto dos testes unitários é testar unidades de código isoladamente . Isso significa que:

  1. Eles não devem quebrar qualquer código não relacionado em outro lugar na base de código.
  2. Apenas um teste de unidade deve quebrar por um bug na unidade testada, ao contrário dos testes de integração (que podem quebrar em pilhas).

Tudo isso implica que toda dependência externa de uma unidade testada deve ser ridicularizada. E eu quero dizer todas as dependências externas , não apenas as "camadas externas", como rede, sistema de arquivos, banco de dados, etc.

Isso leva a uma conclusão lógica de que praticamente todos os testes de unidade precisam ser reproduzidos. Por outro lado, uma rápida pesquisa no Google sobre zombaria revela toneladas de artigos que afirmam que "zombar é um cheiro de código" e deve ser evitado (embora não completamente).

Agora, para a (s) pergunta (s).

  1. Como os testes de unidade devem ser escritos corretamente?
  2. Onde exatamente fica a linha entre eles e os testes de integração?

Atualização 1

Por favor, considere o seguinte pseudo código:

class Person {
    constructor(calculator) {}

    calculate(a, b) {
        const sum = this.calculator.add(a, b);

        // do some other stuff with the 'sum'
    }
}

Um teste que testa o método Person.calculate sem zombar da dependência Calculator (dado que o Calculator é uma classe leve que não acessa "o mundo externo") é considerado um teste de unidade?

    
por Alexander Lomia 27.11.2018 / 11:46
fonte

6 respostas

52

the point of unit tests is to test units of code in isolation.

Martin Fowler no Teste de unidade

Unit testing is often talked about in software development, and is a term that I've been familiar with during my whole time writing programs. Like most software development terminology, however, it's very ill-defined, and I see confusion can often occur when people think that it's more tightly defined than it actually is.

O que Kent Beck escreveu em Test Driven Development, por exemplo

I call them "unit tests", but they don't match the accepted definition of unit tests very well

Qualquer alegação de que "o ponto de testes unitários é" dependerá strongmente de qual definição de "teste unitário" está sendo considerada.

Se sua perspectiva é que seu programa é composto de muitas unidades pequenas que dependem uma da outra, e se você se restringir a um estilo que testa cada unidade isoladamente, então um monte de teste duplica é uma conclusão inevitável.

O conselho conflitante que você vê vem de pessoas operando sob um conjunto diferente de suposições.

Por exemplo, se você está escrevendo testes para apoiar desenvolvedores durante o processo de refatoração, e dividindo uma unidade em duas é uma refatoração que deve ser suportada, então algo precisa ser fornecido. Talvez esse tipo de teste precise de um nome diferente? Ou talvez precisemos de um entendimento diferente de "unidade".

Você pode querer comparar:

Can a test that tests the Person.calculate method without mocking the Calculator dependency (given, that the Calculator is a lightweight class that does not access "the outside world") be considered a unit test?

Eu acho que essa é a pergunta errada a se fazer; é novamente uma discussão sobre rótulos , quando acredito que o que realmente nos interessa são propriedades .

Quando estou introduzindo alterações no código, não me importo com o isolamento de testes - já sei que "o erro" está em algum lugar da minha pilha atual de edições não verificadas. Se eu executar os testes com frequência, limito a profundidade dessa pilha, e encontrar o erro é trivial (no caso extremo, os testes são executados após cada edição - a profundidade máxima da pilha é uma). Mas a execução dos testes não é o objetivo - é uma interrupção -, portanto, há valor na redução do impacto da interrupção. Uma forma de reduzir a interrupção é garantir que os testes sejam rápidos (Gary Bernhardt sugere 300 ms < / a>, mas eu não descobri como fazer isso nas minhas circunstâncias).

Se invocar Calculator::add não aumentar significativamente o tempo necessário para executar o teste (ou qualquer uma das outras propriedades importantes para este caso de uso), então eu não me incomodaria em usar um teste duplo - isso não acontece fornecer benefícios que superem os custos.

Observe as duas suposições aqui: um ser humano como parte da avaliação de custos e o pequeno número de mudanças não verificadas na avaliação de benefícios. Em circunstâncias em que essas condições não se mantêm, o valor do "isolamento" muda um pouco.

Veja também Lava quente , de Harry Percival.

    
por 27.11.2018 / 12:47
fonte
32

How exactly should unit tests be written without mocking extensively?

Minimizando os efeitos colaterais em seu código.

Pegando seu código de exemplo, se calculator , por exemplo, fala com uma API da web, ou você cria testes frágeis que dependem da capacidade de interagir com essa API da web ou cria uma simulação dela. Se, no entanto, é um conjunto de funções de cálculo determinístico e livre de estados, então você não (e não deve) zombar dele. Se fizer isso, você corre o risco de seu mock se comportar de maneira diferente do código real, levando a erros em seus testes.

Os mocks só devem ser necessários para o código que lê / grava no sistema de arquivos, bancos de dados, pontos de extremidade de URL etc; que dependem do ambiente em que você está executando; ou que são altamente estatisticamente e não-determinísticos por natureza. Então, se você mantiver essas partes do código no mínimo e ocultá-las atrás de abstrações, elas serão fáceis de burlar e o resto do seu código evita a necessidade de zombarias.

Para os pontos de código que têm efeitos colaterais, vale a pena escrever testes que simulam e testes que não. Estes últimos precisam de cuidados, pois serão inerentemente frágeis e possivelmente lentos. Portanto, você pode querer executá-las somente durante a noite em um servidor de IC, em vez de salvar cada vez que você salvar seu código. Os primeiros testes devem ser executados sempre que possível. Quanto a se cada teste é, então, um teste de unidade ou integração torna-se acadêmico e evita "guerras flamejantes" sobre o que é e não é um teste de unidade.

    
por 27.11.2018 / 13:34
fonte
30

Essas questões são bem diferentes em suas dificuldades. Vamos fazer a pergunta 2 primeiro.

Testes de unidade e testes de integração são claramente separados. Um teste unitário testa a unidade um (método ou classe) e usa outras unidades apenas o necessário para atingir esse objetivo. Zombaria pode ser necessária, mas não é o ponto do teste. Um teste de integração testa a interação entre diferentes unidades reais . Essa diferença é toda a razão pela qual nós precisamos testes de unidade e integração - se um fizesse o trabalho do outro bem o suficiente, nós não o faríamos, mas é geralmente mais eficiente usar dois ferramentas especializadas em vez de uma ferramenta generalizada.

Agora, para a importante pergunta: Como você deve testar a unidade? Como dito acima, os testes unitários devem construir estruturas auxiliares apenas na medida do necessário . Geralmente é mais fácil usar um banco de dados simulado do que seu banco de dados real ou mesmo qualquer banco de dados real . No entanto, zombando em si não tem valor. Se acontecer com frequência, é mais fácil usar os componentes real de outra camada como entrada para um teste unitário de nível médio. Em caso afirmativo, não hesite em usá-los.

Muitos praticantes têm medo de que, se o teste unitário B reutilizar as classes que já foram testadas pelo teste unitário A, então um defeito na unidade A cause falhas nos testes em vários lugares. Eu considero que isso não é um problema: uma suíte de testes tem que ter sucesso 100% para dar a você a tranquilidade que você precisa, então não é um grande problema ter muitas falhas - afinal, você < em> do tem um defeito. O único problema crítico seria se um defeito acionasse também algumas falhas .

Portanto, não faça uma religião de zombaria. É um meio, não um fim, então se você puder evitar o esforço extra, você deve fazê-lo.

    
por 27.11.2018 / 11:59
fonte
4

OK, então responda suas perguntas diretamente:

How should unit tests be written properly?

Como você diz, você deve estar zombando de dependências e testando apenas a unidade em questão.

Where exactly does the line between them and integration tests lie?

Um teste de integração é um teste de unidade em que suas dependências não são ridicularizadas.

Can a test that tests the Person.calculate method without mocking the Calculator be considered a unit test?

Não. Você precisa injetar a dependência da calculadora neste código e você pode escolher entre uma versão falsa ou real. Se você usa um mocked é um teste unitário, se você usa um real é um teste de integração.

No entanto, uma ressalva. você realmente se importa com o que as pessoas acham que seus testes deveriam ser chamados?

Mas a sua verdadeira questão parece ser esta:

a quick Google search about mocking reveals tons of articles that claim that "mocking is a code smell" and should mostly (though not completely) be avoided.

Eu acho que o problema aqui é que muitas pessoas usam mocks para recriar completamente as dependências. Por exemplo, eu poderia zombar da calculadora no seu exemplo como

public class MockCalc : ICalculator
{
     public Add(int a, int b) { return 4; }
}

Eu não faria algo como:

myMock = Mock<ICalculator>().Add((a,b) => {return a + b;})
myPerson.Calculate()
Assert.WasCalled(myMock.Add());

Eu diria que isso seria "testar minha simulação" ou "testar a implementação". Eu diria " Não escreva Mocks! * assim".

Outras pessoas discordariam de mim, nós começaríamos guerras em chamas em nossos blogs sobre o Best to Mock, o que realmente não faria sentido se você não entendesse todo o contexto das várias abordagens e realmente não oferecesse um todo. muito valor para alguém que só quer escrever bons testes.

    
por 27.11.2018 / 13:12
fonte
3
  1. How should unit tests be implemented properly?

Minha regra é que os testes unitários apropriados:

  • São codificados contra interfaces, não implementações . Isso tem muitos benefícios. Por um lado, ele garante que suas aulas sigam o Princípio de Inversão de Dependência de SÓLIDO . Além disso, isso é o que suas outras classes fazem ( certo? ) para que seus testes façam o mesmo. Além disso, isso permite que você teste várias implementações da mesma interface enquanto reutiliza grande parte do código de teste (somente a inicialização e algumas asserções seriam alteradas).
  • São autônomos . Como você disse, alterações em qualquer código externo não podem afetar o resultado do teste. Como tal, os testes unitários podem ser executados no tempo de construção. Isso significa que você precisa de zombarias para remover quaisquer efeitos colaterais. No entanto, se você está seguindo o Princípio de Inversão de Dependência, isso deve ser relativamente fácil. Os bons frameworks de teste, como Spock , podem ser usados para fornecer dinamicamente implementações simuladas de qualquer interface para usar em seus testes com codificação mínima. Isso significa que cada classe de teste só precisa exercitar código de uma classe de implementação exatamente uma , além da estrutura de teste (e talvez classes de modelos ["beans"]).
  • Não requer um aplicativo em execução separado . Se o teste precisar "conversar com algo", seja um banco de dados ou um serviço da web, será um teste de integração, não um teste de unidade. Eu desenho a linha nas conexões de rede ou no sistema de arquivos. Um banco de dados SQLite puramente na memória, por exemplo, é um jogo justo na minha opinião para um teste de unidade, se você realmente precisar dele.

Se houver classes de utilitários de frameworks que complicam o teste de unidade, você pode até achar útil criar interfaces e classes "wrapper" muito simples para facilitar a zombaria dessas dependências. Esses invólucros não seriam necessariamente sujeitos a testes unitários.

  1. Where exactly does the line between them [unit tests] and integration tests lie?

Eu achei essa distinção a mais útil:

  • Testes de unidade simulam "código de usuário" , verificando o comportamento de classes de implementação em relação ao comportamento desejado e semântica de interfaces de nível de código .
  • Testes de integração simulam o usuário , verificando o comportamento do aplicativo em execução contra casos de uso especificados e / ou APIs formais. Para um serviço da web, o "usuário" seria um aplicativo cliente.

Há área cinzenta aqui. Por exemplo, se você puder executar um aplicativo em um contêiner do Docker e executar os testes de integração como o estágio final de uma compilação e depois destruir o contêiner, será aceitável incluir esses testes como "testes de unidade"? Se este é seu debate ardente, você está em um bom lugar.

  1. Is it true that virtually every unit test needs to mock?

Não. Alguns casos de teste individuais serão para condições de erro, como passar null como um parâmetro e verificar se você recebe uma exceção. Muitos testes como esse não exigirão nenhuma simulação. Além disso, implementações que não têm efeitos colaterais, por exemplo, processamento de string ou funções matemáticas, podem não exigir nenhum erro porque você simplesmente verifica a saída. Mas a maioria das classes que valem a pena, creio eu, exigirá pelo menos uma simulação em algum lugar no código de teste. (Quanto menos, melhor.)

A questão do "cheiro de código" que você mencionou surge quando você tem uma classe excessivamente complicada, que requer uma longa lista de dependências simuladas para escrever seus testes. Esse é um indício de que você precisa refatorar a implementação e dividir as coisas, de modo que cada classe tenha uma pegada menor e uma responsabilidade mais clara e, portanto, seja mais facilmente testável. Isso irá melhorar a qualidade a longo prazo.

Only one unit test should break by a bug in the tested unit.

Eu não acho que isso seja uma expectativa razoável, porque funciona contra a reutilização. Você pode ter um método private , por exemplo, que é chamado por vários métodos public publicados pela sua interface. Um bug introduzido nesse método pode causar várias falhas de teste. Isso não significa que você deve copiar o mesmo código em cada método public .

    
por 28.11.2018 / 00:28
fonte
3
  1. They should not break by any unrelated code change elsewhere in the codebase.

Não tenho certeza de como essa regra é útil. Se uma mudança em uma classe / método / qualquer coisa pode quebrar o comportamento de outra em código de produção, então as coisas são, na verdade, colaboradoras, e não não relacionadas. Se seus testes quebrarem e seu código de produção não, então seus testes são suspeitos.

  1. Only one unit test should break by a bug in the tested unit, as opposed to integration tests (which may break in heaps).

Eu também consideraria essa regra suspeita. Se você for realmente bom o bastante para estruturar seu código e escrever seus testes de tal forma que um bug cause exatamente uma falha no teste unitário, então você está dizendo que já identificou todos os possíveis bugs, mesmo que o codebase evolua para casos de uso. não antecipou.

Where exactly does the line between them and integration tests lie?

Eu não acho que seja uma distinção importante. O que é uma 'unidade' de código?

Tente encontrar pontos de entrada nos quais você pode escrever testes que "façam sentido" em termos do domínio do problema / regras de negócios que esse nível do código está lidando. Frequentemente, esses testes são de natureza "funcional" - insira uma entrada e teste se a saída é a esperada. Se os testes expressarem um comportamento desejado do sistema, eles geralmente permanecerão bastante estáveis, mesmo que o código de produção evolua e seja refatorado.

How exactly should unit tests be written without mocking extensively?

Não leia muito a palavra 'unidade' e incline-se a usar suas classes de produção reais em testes, sem se preocupar muito se você estiver envolvendo mais de um deles em um teste. Se um deles é difícil de usar (porque é preciso muita inicialização, ou ele precisa atingir um servidor real de banco de dados / e-mail, etc.), deixe seus pensamentos se voltarem para o mocking / faking.

    
por 28.11.2018 / 00:55
fonte