Como escrever um bom código e bons testes de unidade?

Índice

Prefácio

O que é teste unitário

O valor do teste unitário

Características do teste unitário

O que os testes unitários medem?

Como escrever testes unitários

Usar simulação

Existem muitas zombarias?


Este artigo tem como objetivo fornecer algumas orientações sobre como escrever testes unitários. Seria ótimo se pudesse ajudá-lo.

Prefácio

Tenho escrito código há vários anos. Muitas pessoas podem saber apenas que existe algo como teste de unidade, mas elas mesmas nunca escreveram um teste de unidade. O teste unitário parece ter sido sempre uma opção, não um requisito, porque mesmo que não exista teste unitário, toda empresa tem pelo menos testadores dedicados. Nós escrevemos o código e depois o colocamos no ambiente de teste e o entregamos aos testadores para verificação. Pode. Isso parece funcionar sem testes de unidade.

No entanto, depois de passar por muitos desvios, descobrimos que mesmo que não consigamos atingir 100% de cobertura de testes unitários, apenas escrever testes unitários para algumas funções complexas ainda pode nos poupar muito tempo. Os principais motivos são os seguintes:

  • No processo de escrever testes de unidade, você pode encontrar alguns bugs no código que você escreveu, nós os verificamos antes de implantá-los no ambiente de teste. Menos bugs chegam aos testadores.
  • Quando há testes unitários, ao corrigir bugs ou adicionar ou modificar funções posteriormente, você pode verificar imediatamente se há algum impacto no módulo antigo após escrever o código.
  • O test drive de unidade pode nos levar a escrever um código melhor, porque se você escrever o código casualmente, descobrirá como testá-lo. Não consigo entender o código que escrevi.
  • O uso de testes de unidade pode tornar nossa verificação mais econômica. Não precisamos executar o aplicativo e depois usar o sistema existente para criar dados ou algo assim para verificar o código que modificamos. Precisamos apenas zombar do código que não tem nada a ver com a situação atual, para que possamos apenas verificar o código que estamos escrevendo no momento.

O que é teste unitário

Pode haver algumas pessoas que conseguem escrever testes, mas podem não escrever bem. Por exemplo, a granularidade do teste é muito grande, como escrever testes diretamente para a interface http. Mas também sabemos que pode haver muitos lógica por trás de uma interface. Desta forma, os testes que escrevemos também conterão muita incerteza, porque qualquer modificação na lógica por trás dessa grande interface pode causar falhas em nossos testes. Esses "testes de unidade" são, sem dúvida, muito frágeis. Conforme mostrado na figura abaixo, anormalidades no servidor RPC, no servidor de banco de dados, no sistema de arquivos e no servidor HTTP farão com que nosso teste falhe. Se estamos escrevendo esse teste agora, teremos que dar uma olhada mais de perto no próximo conteúdo.

Essencialmente, esse tipo de teste não é um teste de unidade, mas um teste de integração.Os testes de unidade não incluem interação com outros componentes, e nossa interface http pode chamar o banco de dados e tem uma forte dependência do banco de dados. Essa dependência também é uma fonte de vulnerabilidade de teste. Em um bom teste de unidade, todas as dependências são simuladas. Ou seja, em nosso código ainda haverá código de acesso ao banco de dados, mas em Ao executar o teste, nenhum código real ocorrerão operações de acesso ao banco de dados. Em um sistema complexo, também pode incluir chamadas RPC, chamadas HTTP, etc., e precisamos zombar dessas coisas fortemente dependentes em testes unitários.

Para entender o teste unitário, você deve primeiro entender o que é uma “unidade”. A chamada "unidade" refere-se à menor unidade de chamada de código, que na verdade se refere a um bloco de função (Função) ou método (Método). **Teste unitário refere-se ao teste dessas unidades de chamada de código. **O teste de unidade é um tipo de teste de caixa branca, que é um teste que deve ser muito claro sobre os detalhes do código da unidade. Portanto, se não escrevermos bem o código, não poderemos escrever os testes. Escrever testes unitários pode nos levar a escrever um código melhor.

Os testes unitários são escritos e executados por engenheiros de software. Comparados aos testes unitários, também existem testes de integração. O teste de integração é basicamente um teste de caixa preta, realizado principalmente por testadores de acordo com o manual de funções do software e requer a cooperação de um ambiente de teste especial.

O valor do teste unitário

Apenas do ponto de vista do teste, o teste unitário é o mais barato e o mais rápido. Como os testes de unidade não possuem dependências externas e podem ser executados diretamente após a gravação, não precisamos preparar um ambiente completo para testes de unidade, como instalar primeiro vários componentes do servidor, executá-los e depois iniciar o aplicativo.

Quando terminarmos de escrever o código, podemos clicar para executar o teste e saberemos imediatamente se há algum problema com nosso código. Porque o teste unitário não precisa depender dessas coisas externas. Portanto, mesmo que ainda não tenhamos o servidor pronto, ainda podemos testar a unidade do nosso código para verificar a exatidão do código.

Além disso, para engenheiros de software, se não houver maneira de verificar rapidamente seu código ao escrevê-lo, não haverá feedback e muitas vezes haverá um forte sentimento de insegurança. Quanto mais código você escreve, mais insegurança acumulará e, eventualmente, perceberá que não tem certeza do código que escreveu. Mesmo com o método de iteração rápida, leva pelo menos uma semana para obter feedback do teste. E é muito provável que os resultados do feedback do teste façam com que você escreva uma semana de código em vão e tenha que derrubar tudo. Portanto, quando os testadores estão testando, os engenheiros de software ficam muito ansiosos. Se o tempo de iteração for maior, a pressão psicológica será maior. Quando os testes estão em andamento, os engenheiros de software geralmente estão ocupados tentando corrigir problemas e também estão sujeitos a conflitos com a equipe de testes, resultando em problemas de comunicação.

É claro que esse problema vai melhorar depois de um período de tempo, porque os bugs sempre serão corrigidos um por um ao longo do tempo. Então poderemos desenvolver alguns novos recursos em um sistema mais estável. Mas ainda é inevitável que funções recentemente desenvolvidas possam quebrar a velha lógica em alguns lugares muito secretos, e então serão descobertas após um período de tempo. Este pode não ser o resultado que queremos ver.

Além disso, uma vez escritos os testes unitários, eles podem ser usados ​​por um longo tempo, especialmente durante a regressão, o que pode ajudar a economizar muito tempo de teste.Podemos saber facilmente se novas funções ou modificações no código antigo danificaram as funções originais. O teste unitário pode ajudar a encontrar muitos problemas ocultos.

Em geral, os testes unitários podem nos trazer o seguinte valor:

  • Menor custo e verificação mais rápida. (Não depende de nenhum ambiente real)
  • Reduza o tempo de teste de regressão. (O teste de unidade pode garantir que a funcionalidade antiga não seja afetada)
  • Isso nos leva a escrever um código melhor. Um código bem projetado facilita a escrita de testes unitários.
  • O teste unitário é essencialmente um documento que descreve a intenção por trás do código que escrevemos.
  • Torne a refatoração subsequente mais segura. (O teste de unidade pode verificar se a refatoração contém bugs)
  • Encurte o ciclo de feedback e reduza os custos de reparo de defeitos. (O feedback pode ser obtido durante a fase de desenvolvimento, quando o custo do reparo é mais baixo)
  • Melhore a velocidade de entrega de software garantindo a qualidade. (Menos bugs, iteração mais rápida)

Características do teste unitário

  • Rápido: deve levar muito pouco tempo para executar testes de unidade.

Se obtivermos alguns projetos de código aberto excelentes, podemos executar os testes de unidade neles e descobrir que os testes de unidade de todos os códigos foram concluídos em alguns segundos.

  • Autônomo: ​​os testes de unidade são independentes e podem ser executados de forma independente. Não depende de outros testes.

Um dos benefícios da independência é que podemos verificar de forma independente se uma determinada lógica está correta. Se precisarmos confiar em outros testes, significa que nosso código ainda apresenta algumas falhas de design. Porque isto parece, até certo ponto, haver uma forte dependência entre as nossas diferentes lógicas.

  • Repetível: os resultados da execução de testes unitários devem ser consistentes (idempotentes)

Se os resultados que executamos forem sempre diferentes, não poderemos fazer afirmações sobre os resultados do programa e não poderemos julgar se os resultados da operação estão corretos.

  • Autoverificação: O teste deve ser capaz de detectar automaticamente se o teste foi aprovado ou reprovado sem qualquer interação humana.

Por exemplo, não podemos dizer para executar um teste e depois verificar se o banco de dados foi gravado com sucesso e se o arquivo foi gravado com sucesso. Porque não há nada fácil de testar esse tipo de coisa. Contanto que o banco de dados possa ser executado corretamente, ele pode definitivamente ser escrito. Se o banco de dados não puder ser escrito sob certas circunstâncias anormais, não é um bug em nosso código, então nós irá zombar do acesso ao banco de dados. O mesmo se aplica à leitura e gravação de arquivos, chamadas RPC, etc.

O que os testes unitários medem?

Como dissemos acima, teste unitário é um teste de um bloco funcional (Função) ou método (Método). Mas nem todas as “unidades” requerem testes unitários. Como queremos fazer testes unitários, precisamos saber o que testar. Por exemplo, o código a seguir precisa ser testado?

public static Response get(String url) throws IOException {
    okhttp3.Request request = new okhttp3.Request.Builder()
            .url(url)
            .build();

    return client.newCall(request).execute();
}

Este é um código de chamada http muito comum, que apenas chama a biblioteca okhttp para iniciar uma solicitação GET com base no URL passado. Se você deseja fazer um teste de unidade, o que exatamente ele está testando? Testar se o servidor por trás desse URL está funcionando corretamente? Testar se minha rede local está normal?

Na verdade, este tipo de dependência de sistemas externos não requer testes, desde que possa ser compilado e aprovado, o sistema operacional garantirá sua execução normal. Se não puder ser executado normalmente, não é um problema com nosso código, pode ser que o servidor onde a URL está localizada esteja inoperante ou a rede local esteja anormal. Mas isso não tem nada a ver com o fato de nosso código poder lidar corretamente com a lógica.

A lógica de negócios que escrevemos não pode lidar com erros quando o servidor externo está inativo. Por exemplo, nosso código calcula 1+1 e afirmamos que é igual a 2. Em seguida, iniciamos uma solicitação HTTP, mas a solicitação HTTP é anormal. Bem , não posso dizer que 1+1 não seja igual a 2 neste momento. Porque a nossa lógica 1+1=2 não tem nada a ver com o sistema externo.

**O teste de unidade testa o código de lógica de negócios que escrevemos. **Todas as interações com sistemas externos não precisam ser testadas.

Como escrever testes unitários

Depois de esclarecer o que queremos testar, precisamos aprender como escrever testes de unidade: fornecendo as entradas e os resultados esperados e comparando-os com os resultados reais de execução da unidade, podemos saber se a unidade está funcionando conforme o esperado. .

Portanto, existem três etapas para escrever testes unitários:

  • Construa parâmetros de entrada e preveja a saída produzida por essa entrada.
  • Chame o método de destino a ser testado e obtenha a saída.
  • Verifique se a saída do método de destino é consistente com a saída esperada (assert assertion).

Para o mesmo método alvo, através da construção de várias entradas, repita os passos acima para detectar se várias condições normais e de contorno são consistentes com as expectativas, garantindo que todas as possibilidades do método alvo sejam cobertas.

Aqui está um exemplo simples (PHP):

// 单元测试的目标方法
function add(int $a, int $b): int
{
    return $a + $b;
}

// 单元测试
// 测试 add 方法
public function testAdd()
{
    // 构建输入
    $a = 1;
    $b = 1;

    // 调用目标方法
    $sum = $this->add($a, $b);

    // 比对输出与期望的值是否一致。
    // 如果不一致的话,单元测试不通过,说明我们的目标方法有错误或者我们的期望值有错误。
    $this->assertEquals(2, $sum);
}

Descobrimos que escrever testes unitários não parece tão difícil, certo? É claro que a maioria dos requisitos no trabalho real são muito mais complicados do que isso, mas as etapas do teste unitário são, na verdade, as três mencionadas acima: construir a entrada, chamar o método em teste e verificar a saída.

Usar simulação

Na verdade, o teste de unidade não é complicado. O que é complicado é, na verdade, o nosso código. Se quisermos escrever melhor testes unitários, também devemos entender os mocks em testes unitários.

Mock é uma tecnologia que nos ajuda a simular métodos de classe em testes unitários. Sabemos que os testes unitários não devem ter dependências de componentes externos, como bancos de dados, então como podemos implementá-los para que os testes unitários não tenham dependências externas? A resposta é simulada. Quando nosso código precisa depender de uma determinada classe, podemos usar a biblioteca simulada para gerar um objeto simulado. Quando nosso código precisa chamar certos métodos desse objeto, ele não produzirá realmente a chamada real. Isso é um pouco abstrato, mas aqui está um exemplo muito típico:

class Adder
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

class Calculator
{
    private $adder;

    /**
     * @param Adder $adder 代表一个对外部的依赖
     */
    public function __construct(Adder $adder)
    {
        $this->adder = $adder;
    }

    public function add($a, $b)
    {
        // 这里只使用了外部依赖,实际中可能包含非常多的逻辑
        return $this->adder->add($a, $b);
    }
}

// 单元测试
public function testCalculator()
{
    // 创建一个模拟的 Adder 对象
    $adder = Mockery::mock(Adder::class)->makePartial();
    // shouldReceive 表明这个 mock 对象的 add 方法会被调用
    // once 表明这个方法只会被调用一次(没有 once 调用表示可以被调用任意次数)
    // with 如果调用 mock 对象的时候传递了 1 和 2 两个参数,就会返回 andReturn 中的参数
    $adder->shouldReceive('add')->once()->with(1, 2)->andReturn(3);

    $c = new Calculator($adder);
    $this->assertEquals(3, $c->add(1, 2));

    $adder = Mockery::mock(Adder::class)->makePartial();
    // 没有指定 with,传递任意参数都会返回 3
    $adder->shouldReceive('add')->andReturn(3);
    $c = new Calculator($adder);
    $this->assertEquals(3, $c->add(2, 3));
}

Em todas as linguagens de programação comuns, haverá uma biblioteca simulada relativamente madura, como:

  • Mockery em PHP (o exemplo acima usa Mockery)
  • Mockito em Java
  • testemunhar em go também fornece função simulada

Com o Mock, podemos atingir o objetivo de isolar dependências externas. Quer se trate de RPC, banco de dados ou leitura e gravação de arquivos, podemos usar um objeto simulado para simular a operação real. Isso significa que não importa como o sistema externo mude, se nosso teste de unidade for aprovado, significa que o código que escrevemos está logicamente correto. Isso tornará nossos testes unitários mais robustos.

 Durante o teste de unidade, geralmente injetamos dependências externas em nosso código na forma de simulações. Haverá grandes diferenças na implementação de várias linguagens, e às vezes isso está relacionado ao framework utilizado:

  • O Laravel do PHP pode simular um objeto, vinculá-lo ao contêiner e, em seguida, usar app() para usar a função de injeção de dependência fornecida pelo framework, ou simular você mesmo e criar diretamente uma nova instância para teste.
  • A injeção de dependência do Spring Boot do Java é mais avançada. A injeção pode ser obtida diretamente adicionando a anotação @Mock/@InjectMocks ao campo de classe.

Existem muitas zombarias?

Depois de ler a descrição acima, podemos ficar entusiasmados em escrever testes de unidade. Depois de começarmos a escrever testes unitários, podemos nos sentir muito frustrados. Depois de escrever um teste por dia com uma xícara de chá e um cigarro, descobriremos por que precisamos zombar de tantas coisas. Neste momento, podemos começar a pensar se esta abordagem zombeteira está certa ou não, e por que é tão trabalhoso escrever?

Quando isso acontece, muitas vezes reflete problemas no design por trás do nosso código. Se uma classe precisa depender de muitas outras coisas, isso significa que a classe em si é muito complexa. O que fazer neste momento? Claro, contanto que você possa correr! Contanto que um dos códigos e a pessoa possam ser executados, tudo ficará bem.

Podemos não ser capazes de fazer nada sobre o código do sistema legado, mas para o nosso novo código, ainda temos a oportunidade de melhorá-lo.No processo de escrever novo código e escrever testes de unidade ao mesmo tempo, podemos pensar sobre como escrever código que pode ser escrito em teste de unidade. Podemos dar uma olhada em algumas coisas sobre design de software, como "A Beleza do Design de Software", de Zheng Ye. Pessoalmente, acho que é bastante realista. Escrever testes unitários continuamente nos permite escrever código reutilizável e generalizável e melhorar nosso design de software.

Acho que você gosta

Origin blog.csdn.net/GDYY3721/article/details/132305622
Recomendado
Clasificación