Dependência de cenários em testes de sistema

Dependência entre testes, sejam eles de unidade, integração ou sistema, são sempre complicadas. A pergunta era a seguinte: “Como fazer os testes de fluxos dependentes? Ex: O teste do incluir é independente, ou seja, não depende de um predecessor. Já o teste de consulta depende do incluir, o de alterar depende do incluir e consultar e o excluir depende do incluir e consultar. Nesses casos incluímos no BDD os passos dos predecessores ou assumimos que eles já existem e na codificação dos testes fazemos a criação da massa de dados no início de teste?”

Não sei se há uma boa resposta pra isso. Vou listar abaixo as estratégias que já utilizei e o que achei de cada uma delas.

1) Ter dados já prontos no banco de dados

Uma primeira estratégia seria já ter dados prontos no banco de dados, e os testes somente fazerem uso dele. Assim, o teste de “listagem” já tem elementos ali para validar, e o teste de “exclusão” já tem um objeto para ser excluído.

Nesse caso, existe a necessidade de manter o banco de dados sempre em um estado válido para o teste. Você pode fazer com que todo teste limpe o banco de dados inteiro e re-insira os dados novamente, através de um .SQL que é enviado pro banco a cada teste executado, ou coisa do tipo. O problema é justamente manter esse “script inicial”: se ele for em SQL, e uma tabela mudar, você será obrigado a mexer direto no .SQL, e isso é trabalhoso; programaticamente é melhor.

Mesmo assim, existem diversos casos/caminhos diferentes a serem testados, e você deverá estar atento para que um teste não passe a quebrar por causa de um novo cenário escrito.

2) Montar o cenário completo em cada teste

Uma alternativa seria fazer com que o teste da listagem navegasse pela tela de inserção antes. Ou seja, seu teste inseriria primeiro o elemento, depois iria para a tela de listagem e verificaria que o ítem está lá; o teste da exclusão, adicionaria um ítem primeiro, para depois excluí-lo.

A vantagem é que é razoavelmente fácil montar o cenário, já que você vai passear pela própria aplicação. A desvantagem é que sua bateria de testes vai levar o triplo do tempo para ser executada, afinal agora o teste navegará por muito mais partes do seu teste.

Aqui você precisará ser mais carinhoso na escrita dos seus testes e Page Objects. Ou seja, no teste da inserção, faz sentido você ir passo-a-passo, setando campo a campo do formulário. Mas no teste da exclusão, você deve ter métodos auxiliares, como por exemplo um insere(“campo”, “campo”, “campo”),  que escondam toda essa complexidade e faça toda a mágica de navegar pela interface. Dessa forma, fica mais fácil reutilizar o comportamento de “inserção” em testes que tem isso como pré-requisito.

3) Disponibilizar serviços web para criação de cenários

Uma outra alternativa é você criar conjuntos de URLs na aplicação que está sendo testada, que monte o cenário específico daquele teste. Dessa forma, você utilizaria as mesmas entidades, DAOs, Builders, e etc, para montar o cenário sem muito trabalho. Seu teste então faria uma requisição para essa URL específica, e depois navegaria pela aplicação.

Nesse caso, além de precisar ter acesso a aplicação original, você deve ser cuidadoso para que esse conjunto de URLs estejam disponíveis apenas no ambiente de testes. A evolução também é mais natural, já que na hora que o programador criar um novo atributo em uma entidade, ele precisará refletir isso nos DAOs e em todos lugares que usam aquela entidade, inclusive no teste.

Seu teste executará mais rápido do que no caso anterior, já que a parte “grossa” de montar o cenário será feita direto no servidor, sem a necessidade de se navegar pela web.

Tem mais?

O que você acha? Enxerga mais alguma? Vantagens e desvantagens das estratégias que listei acima?

Esse post foi motivado pela mensagem no grupo de discussão do meu livro. Você pode vê-la aqui.

9 thoughts on “Dependência de cenários em testes de sistema

  1. Elias Nogueira

    Olá Maurício!
    Muito bom os teus pontos, ainda mais que estes pontos estão sendo colocados por um desenvolvedor 😛
    Teoricamente, e até na prática, não precisariamos chamar uma função de inclusão quando efetuassemos uma exclusão se nesta suite tivermos um certo sequenciamento das atividades.
    Como tu mesmo comentou há sempre pré-condições em outras ações de CRUD para pesquisa e exclusão, Uma vez que tenhamos essa suite sequencial conseguimos poupar a execução de métodos e até mesmo o número de inserção de dados.
    Eu sempre crio uma suite sequencial de CRUD com o seguinte sequenciamento:
    – Inserção
    – Busca
    – Remoção

    Pra mim é um modo de escrever menos código e ainda sim garantir a dependência.

    Abraço!

    Reply
    1. mauricioaniche Post author

      Oi Elias,

      Tem sido uma alternativa nossa aqui também. Escrever testes maiores, que passem pelos cenários em uma ordem pré-definida. Isso facilita a escrita do teste, a manutenção, mas dá mais trabalho nas asserções, afinal precisa dar um jeito de, quando o teste falhar, saber o ponto exato que aconteceu!

      Reply
  2. Rafael Ponte

    Olá Aniche,

    Sempre que trabalhei com testes de sistema eu segui algo semelhante a 1a estratégia: preparava o banco antes de rodar cada teste, porém a diferença é que eu seguia o modelo de um teste de integração, isto é, cada teste era encarregado de limpar e preparar o banco de acordo com seu cenário – e não um scriptzão com todo o banco pronto!

    E por sorte nunca tive que utilizar SQL, mas sim datasets do framework DbUnit – no final é um XML simples para popular e limpar registros e tabelas. Provavelmente você já o conhece!

    A 2a estratégia torna rodar a bateria de testes muito mais lenta e demorada (como você mencionou), mas tem algo que você não mencionou: trabalhar dessa forma pode ocasionar os famigerados falsos-positivos – digamos que para testar a exclusão eu deva inserir um item antes; se durante a inserção o teste quebrar não fica claro se o que quebrou foi de fato a exclusão (funcionalidade que estou testando).

    Já vi equipes trabalharem com a 2a estratégia de uma maneira diferente: eles escreviam testes de fato dependentes, ou seja, a ordem dos testes influenciava completamente a bateria. Explico, para testar a edição de um item um teste de inserção do item deveria rodar antes, caso não rodasse o primeiro teste quebrava.

    Não gosto dessa abordagem pois acredito que qualquer teste deva ser independente, isolado e repetível. Mas ao conversar com uma equipe de Q&A eu percebi que a mesma independência que buscamos nos testes de unidade e integração não agregam tanto valor para testes de sistemas escritos por uma equipe de Q&A.

    Uma vantagem interessante da 2a estratégia é que ela facilita a vida da equipe de Q&A, que muitas vezes não tem um domínio tão bom em programação (Java+Selenium por exemplo) nem com SQL. Por mais lento que a bateria fique com o tempo, o desenvolvimento dos testes automatizados pela equipe de Q&A são facilitados.

    A 3a estratégia é algo que até acho interessante em cenários onde existem regras complexas para preparar os dados e você gostaria de aproveitar o código de produção para isso, mas no geral eu preferiria que cada teste preparasse seu cenário em vez de solicitar à uma URI.

    O e-mail que eu fiquei te devendo está relacionado a independência dos testes de sistemas quando estes são escritos por uma equipe de Q&A – não sei se vale realmente a pena ou não mante-los independentes e isolados. Acho que preciso de mais discussão sobre o assunto.

    Enfim, excelente post, Aniche!

    Reply
    1. Hudson Leite

      Mauício, parabéns pelo post, muito boa discursão.

      Interessante observar o comentário do Rafael Ponte, pois também creio que testes devam ser independentes (auto-contidos) usando o “setup” para criar o contexto necessário bem como o “teardown” para “limpar a bagunça feita na casa” antes que o próximo “cliente”(teste) chegue (algo parecido com deixar o vaso do sanitário do jeito que ele estava quando foi encontrado para a próxima pessoa utilizar sem traumas). Talvez refatorar o código, de forma a tornar os passos necessários a preparação do contexto mais auto-explicativos, seja uma opção (algo parecido com o que é feito em [Growing Object-Oriented Software Guided by Tests]), tipo:
      Dado que existe fulado

      Quando clico em excluir
      Então fulano não mais existe

      //Vaso está limpo, e sempre estará limpo antes que alguém o utilize
      @Before public void inicializarEstadoDoBanco(){
      DbRunner.inicializar();
      }
      @Dado(“que existe fulado”) public void garantirQueExisteFulano(){
      DbRunner.inserirFulano();
      }

      @Então(“fulano não mais existe”) public void garantirFulanoNaoMaisExiste(){
      assertTrue(DbRunner.fulanoNaoExiste());
      }

      Bom, é só minha opinião, e pode estar coberta de coliformes fecais.
      Abraço.

      Reply
  3. Alexandre Aquiles

    Onde trabalho, adotamos uma abordagem semelhante à citada pelo Rafael. Usamos datasets com DBUnit.

    Duas dicas no uso do DBUnit:
    * Usar datasets em formato CSV: permite controle de versão efetivo, é possível editar em Excel (ou semelhante) e é facilmente exportável de vários BD.
    * Em caso de exceção, é importante reverter as alterações no estado do BD. Por isso, use transações nos dataloaders.

    ____

    Coisas que temos aprendido sobre testes de sistema (que chamamos de funcionais):

    Como nosso sistema foi desenvolvido há algum tempo e sem testes automatizados, temos o papel de automatizador, um cara técnico responsável por criar testes funcionais e de integração (que são mais subcutâneos e usam a API do sistema diretamente).

    Uma coisa que percebemos é que automatizar requisitos ainda instáveis não é uma boa. As docs do WebDriver [1] descrevem algo parecido.

    Para processamentos de arquivos, focamos em testes de integração. Estamos avaliando testar as operações de CRUD nesses nível, para agilizar o feedback.

    Testes funcionais são lentos e instáveis, principalmente no caso de nosso app, que usa muito Ajax. Dar um refresh na página antes de cada teste funcional se mostrou uma boa prática, pra evitar que testes que sujam o DOM façam outros falharem. Uma coisa bem bacana é tirar screenshots de testes que falham.

    E sempre tenham testes manuais exploratórios!
    ____

    Uma coisa que acho interessante discutir é a ideia de pirâmide de testes [2]. Segundo essa ideia, é interessante ter mais testes unitários que de integração e mais de integração que funcionais.

    Só que no caso de apps que são compostas basicamente por CRUDs, parece que testes de integração são mais interessantes. Aí ficaria tipo um “losango de testes”. Parece que é a abordagem usada em apps Rails, por exemplo. O que vocês acham da ideia?

    [1] http://docs.seleniumhq.org/docs/01_introducing_selenium.jsp#to-automate-or-not-to-automate
    [2] http://watirmelon.com/tag/testing-pyramid/

    Reply
    1. Mauricio Aniche

      Eu acho que faz todo sentido! Acredito que a “figura geométrica” de testes é você que tem que decidir, dado o contexto do projeto! O ponto é saber quando usar qual, certo?

      Um abraço!

      Reply
  4. Vladson Freire

    Aniche, desculpe pela demora do meu feedback, mas tive um problema pessoal muito sério no final da semana passada.

    O post foi muito interessante. Você conseguiu falar de integração em todos os níveis que ela acontece.

    No meu contexto atual, a primeira opção seria a menos indicada, pois trabalho em fábrica de software com fases bem definidas e mantidas por CMMI, projetos de diversos clientes e tecnologias distintas.

    A segunda, ficou bem na linha de pensamento que estava seguindo, pois se estou recebendo de uma 3º pessoa as histórias de testes com BDD e Cucumber, faz sentido criar uma sequência lógica onde os cenários de testes completassem uns aos outros. Contudo, cada testes deve se preocupar apenas com as suas regras, por isso, estou orientando a equipe a escrever etapas da seguinte forma:

    Dado que estou na tela Consultar Cliente
    Então Clico no botão ‘Incluir’
    E cadastro um Cliente chamado ‘Vladson’……
    Então volto para a tela Consultar Cliente
    E “Inicia o Teste da tela de consulta”

    Usar o DBUnit ou SQL, faz sentido para testes de integração e não para testes de comportamento, pois para Excluir o Testador Insere e Consulta antes de realizar a exlusão e isso é comportamento.

    Obrigado pelas respostas,
    Vladson Freire

    Reply
    1. Mauricio Aniche

      Oi Vladson,

      É isso aí. Se naquele teste, a inclusão não é importante, faz sentido vc “esconder” ela em apenas 1 linha de Cucumber! 🙂

      Um abraço,
      Mauricio

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *