Testes de software: conteúdo teórico inicial

Investigando código

Testes de software: conteúdo teórico inicial

Este breve artigo inicia a nova categoria de artigos neste blog: Testes de software.

Cada vez mais, é nítido que empresas têm adotado ferramentas de testes, visando melhorar a qualidade de códigos desenvolvidos e a confiabilidade de seus sistemas. Estas ferramentas ajudam a diminuir problemas de dívidas técnicas, que são antigos desafios da engenharia de software.

Existem categorias de testes diferentes para diferentes estágios do desenvolvimento. As categorias vão desde aqueles que são praticamente utilizados por desenvolvedores, como testes unitários, até aqueles que são testes de aceitação contando com a participação e aprovação do cliente.

Este conteúdo dá enfase a testes unitários aplicados a programação orientada a objetos e desenvolvidos na linguagem PHP. Talvez alguns detalhes poderão ser aplicados a testes funcionais. Contudo, outras categorias de testes também poderão ser cobertos em artigos posteriores neste blog.

É importante observar o termo ‘teste’ sendo muito rico e podendo ter muitas aplicações e significados. Por isto, neste artigo, em alguns momentos, o termo teste pode se referir a uma classe de testes que agrupa todos os métodos de teste com suas asserções. Mas em outros momentos, o termo teste também pode se referir a um único método de uma classe de teste. Acima de tudo, teste é também um verbo, uma ação que pode ser exercida. Por este motivo, este artigo poderá ser revisado.

A maioria dos leitores deste artigo, são programadores e talvez você também seja. Por esta razão, o texto deste artigo muitas vezes poderá envolver você, como estando interessado em realizar testes ou simplesmente lhe dando um conselho sobre o que se deve fazer ou não.

Para iniciar nossa breve viagem nesta área da engenharia de softwares, é interessante começar pela estrutura. Todo teste unitário, construido como método em uma classe de testes, é divido em três partes, sendo ao menos três etapas:

  • Preparação: etapa onde se gera a instância da classe que será testada. Como você sabe, muitas classes possuem uma ou mais dependências a serem satisfeitas.  Então, é nesta etapa que se cuida deste detalhe.
  • Execução: uma vez tendo tudo preparado, poderemos executar o teste por invocar o método alvo do teste, e assim obter um resultado ou capturar um comportamento, que será comparado a um valor ou comportamento esperado.
  • Asserção: etapa final do teste, onde se faz a comparação entre o resultado obtido na execução com o resultado que é esperado.

A ordem das etapas em algum momento pode não ser tão óbivas assim, principalmente em testes onde as asserções são comportamentais, mas isso será assunto para outro artigo.

É importante entender que ao se realizar testes unitários, somente uma classe deve ser alvo do teste. Além disso, muitas vezes não será necessário invocar mais do que um método para obter um resultado. O teste unitário, mesmo sendo repetitivo aqui, é importante ser lembrado como o nome indica, que é um teste em uma unidade mínima de sistema, no máximo a nível de classe. Se você envolver outras classes, deixará de ser um teste unitário para se tornar um teste de integração. Observe que há uma exceção: será sempre quando fazemos uso de classes como dublês.

Se você irá programar testes unitários, precisa ficar atento para não produzir testes viciados! Para evitar este tipo de teste, uma única asserção não será suficiente para garantir confiabilidade. Você deverá criar cenários de testes que venham cobrir a maioria das situações. Por exemplo: imagine uma classe para validação, onde se espera um resultado com tipo booleano, somente sendo verdadeiro ou falso. Cenários diferentes, deveriam testar a classe de validação em condições que retornam o resultado verdadeiro e que também retornam o resultado falso. Contudo, mesmo que o retorno possibilite um de apenas dois estados [verdadeiro ou falso] por vez, poderiamos contar com ao menos três cenários de testes que variariam conforme o argumento recebido a ser processado:

  • um cenário de teste que informe um valor correto para que a validação seja bem sucedida, retornando verdadeiro
  • um cenário de teste que informe um valor incorreto para que a validação não seja bem sucedida, retornando falso
  • um cenário de teste que não informe valor para a validação não seja bem sucedida, retornando falso

Visto que o teste unitário, não permite uso de outras classes, como então injetar as dependências da classe a ser testada? Como deve feito a preparação de um teste unitário sem que ocorra uma violação de regra? A resposta está no uso de Dublês de objetos para representarem os objetos reais. Aqui pode ser até contraditório mencionar que, dependendo do tipo de dublê escolhido, poderíamos criar uma classe a parte para retornar valores estáticos. Nesta condição, não deixaria de ser um teste unitário, porque a classe dublê teria sido criada exclusivamente com o propósito de ser utilizada pelo teste. Mas isto também não entra em discussão aqui neste artigo.

Como os dublês funcionam?

Alguns dublês são criados apenas para satisfazer as dependências necessárias. Mas outros terão uma participação efetiva nos processos alvo do teste.

Com relação aos dublês que apenas satisfazem dependências, note a classe abaixo:


namespace App\Models\Cadastro; 

use Framework\Core\System\Model;
use Framework\Core\Database\Connection;

class Produto extends Model
{
    
    protected Connection $conn;
    protected array $data = [];

    public function __construct(Connection $conn)
    {
        $this->conn = $conn;
    }

    public function setDescricao (string $descricao): Produto
    {
        $this->data['descricao'] = strtoupper($descricao);
        return $this;
    }
} 

Suponha que a classe Produto receba um teste para o método setDescricao, para examinar se o método está se comportando conforme o esperado. A classe poderá ser instânciada se resolvermos sua dependência na etapa de preparação. Mas, o método setDescricao, alvo do teste, não faz uso do objeto Connection injetado como dependência. Logo, o dublê necessário aqui servirá apenas para atender a dependência que deve ser injetada no construtor. Este tipo de dublê é conhecido com Dummy, porque estará ali apenas como coadjuvante. É semelhante a situação quando você assiste um filme, com alguma cena em uma via pública, e outras pessoas são colocadas no cenário para ficar andando para lá e para cá só para dar vida a cena, mas nada interfere no papel dos personagens principais que estão em foco na cena.

Mas, e que dizer quando um objeto injetado como dependência da classe a ser testada, tem participação efetiva no método que será alvo do teste? note a próxima classe:


namespace App\Validations\Comentarios;

use App\Repositories\Comentarios\ListaNegraRepository;

class ComentarioValidator 
{
    
    protected ListaNegraRepository $listaNegraRepository;

    public function __construct(ListaNegraRepository $listaNegraRepository) 
    {
        $this->listaNegraRepository = $listaNegraRepository;
    }

    public function verficarSeExistePalavraProibida(string $comentario): bool
    {
        $palavrasProibidas = $listaNegraRepository->getAllAsArray();
        
        foreach($palavrasProibidas as $palavra)
        {
            if($this->encontrar($palavra, $comentario)) {
	        return true;
            }
        }
        return false;
    }

    private function encontrar(string $palavra, string $comentario): bool
    {
        return !! strpos($palavra, $comentario);
    }
}

A classe em questão tem uma dependência que, deverá ser tratada na etapa de preparação.

Suponha que o teste seja aplicado no método verficarSeExistePalavraProibida. Note que, diferente do que aconteceu no exemplo anterior, a dependência agora será essencial para funcionamento do método alvo do teste. Quando analisamos a classe ComentarioValidator e examinamos o método verficarSeExistePalavraProibida, percebemos primeiramente que conforme se informa como argumento uma string comentario, a expectativa deve ser de obtermos como resultado um valor booleano que pode ser verdadeiro ou falso, dependendo do processamento. Onde, entra o uso do dublê?

Vamos voltar a examinar o funcionamento da classe, independente do uso do dublê. Para que o processamento no método verficarSeExistePalavraProibida da classe ComentarioValidator aconteça, quando o objeto original ListaNegraRepository é injetado na classe, este método verficarSeExistePalavraProibida primeiro o faz uso do método getAllAsArray de ListaNegraRepository para recuperar todas as palavras que estão na tabela do banco de dados. Esta tabela representa a lista negra de palavras proibidas. O método getAllAsArray devolve os dados desta tabela como um array, que contem cada palavra como um elemento deste array resultante. Em seguida, o array será iterado em um loop foreach para que, a cada iteração, o método encontrar, que é um método de suporte da classe ComentarioValidator, seja invocado recebendo como argumentos tando o elemento corrente do array na iteração, quanto o comentario já informado no início de tudo. O método encontrar tenta encontrar uma posição na string comentario, para string palavra, que vem de a cada iteração do loop foreach. Se encontrar uma posicao para a palavra na string comentário, devolverá um valor booleano verdadeiro como resultado. Caso contrário, após terminar todas as iterações de foreach, o retorno padrão booleano falso será retornado como resultado.

Veja que o dublê agora deve ter uma participação efetiva, agindo no coração do método alvo do teste, e deve simular o método getAllAsArray de ListaNegraRepository, devolvendo um array de palavras. Este tipo de dublê é conhecido como Mock e sempre é utilizado para devolver um resultado fixo, independente do tipo retornado. Uma vez tendo um Mock como dublê da classe ListaNegraRepository, teremos o que precisamos para criar os cenários de testes que serão no mínimo quatro. Os argumentos serão variados no teste para produzir cenários distintos que deverão cobrir a maioria ou todas as situações.

Mas, você deve estar se perguntando: se o método verficarSeExistePalavraProibida produzirá como resultado apenas um valor booleano, que pode ser verdadeiro ou falso, por que serão necessários quatro cenários? Observe que este exemplo se parece muito com aquele exemplo bem mais genérico no início deste artigo e, fazendo pequenos ajustes, temos estes cenários:

  • um cenário de teste que informe um valor correto para que a validação não seja bem sucedida, retornando falso
  • um cenário de teste que informe um valor incorreto para que a validação seja bem sucedida, retornando verdadeiro
  • um cenário de teste que não informe valor para a validação não seja bem sucedida, retornando falso

Além de mais um cenário para banco de dados:

  • um cenário de teste que informe um valor mas não receba dados da tabela do banco de dados e a validação não seja bem sucedida, retornando falso

Talvez estes ainda não sejam suficientes.

Até aqui foi explicado a ideia de dois dublês de testes conhecidos como Dummy e Mock. Contudo, ainda existem outros dublês utilizados para outros testes específicos. Imagine uma outra situação, onde você precisa de uma espécie de Mock que devolva valores de forma dinâmica, funcionando muito mais parecido com o objeto real? Este é o Faker.

O Faker é um tipo de dublê de testes que apresenta um comportamento semelhante a classe real, mas limitado ao tempo de execução do teste. Um exemplo prático seria a simulação de operações de CRUD em tabelas de banco de dados, tais como a de criar, ler ou até excluir registros. Não é difícil imaginar aqui que são utilizados arrays, junto a uma certa lógica, para dar vida a esta simulação. Após o termino de execução do teste, os dados utilizados em memória serão descartados. O Faker normalmente é um método que recebe parâmetros para aplicar os cenários distintos, mas pode ser uma classe dublê criada só para o propósito de teste (Você já leu algo bem genérico sobre isto aqui). Os resultados serão gerados conforme a variação dos parâmetros informados no Faker. O Faker será abrangido em um próximo artigo junto ao Spy, que tem um comportamento bem peculiar ao seu nome. Uma dica para alimentar a curiosidade sobre o Spy: Seria o dublê ideal para testar a primeira classe dos exemplos anteriores.

Há muito sobre o que se tratar sobre testes e seus dublês.

Você deve também estar muito interessado em como são executados estes testes. Os artigos futuros nesta categoria poderão superar suas expectativas!

[]’s

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.