Como testar classes de resumo de unidade: estender com stubs?


446

Eu queria saber como testar as classes abstratas de unidade e classes que estendem classes abstratas.

Devo testar a classe abstrata estendendo-a, removendo os métodos abstratos e testando todos os métodos concretos? Em seguida, apenas teste os métodos que eu substituo e teste os métodos abstratos nos testes de unidade para objetos que estendem minha classe abstrata?

Devo ter um caso de teste abstrato que possa ser usado para testar os métodos da classe abstrata e estender essa classe no meu caso de teste para objetos que estendem a classe abstrata?

Observe que minha classe abstrata tem alguns métodos concretos.

Respostas:


268

Escreva um objeto Mock e use-o apenas para teste. Eles geralmente são muito, muito, muito mínimos (herdados da classe abstrata) e não mais. Então, em seu Teste de unidade, você pode chamar o método abstrato que deseja testar.

Você deve testar a classe abstrata que contém alguma lógica, como todas as outras classes que você possui.


9
Porra, eu tenho que dizer que esta é a primeira vez que eu concordo com a idéia de usar uma farsa.
Jonathan Allen

5
Você precisa de duas classes, uma simulação e teste. A classe mock simula apenas os métodos abstratos da classe Abstract em teste. Esses métodos podem não funcionar, retornar nulo etc., pois não serão testados. A classe de teste testa apenas a API pública não abstrata (ou seja, a interface implementada pela classe Abstract). Para qualquer classe que estenda a classe Abstract, você precisará de classes de teste adicionais porque os métodos abstratos não foram abordados.
cyber-monk

10
Obviamente, é possível fazer isso .. mas, para testar verdadeiramente qualquer classe derivada, você testará repetidamente essa funcionalidade básica .. o que o levará a ter um acessório de teste abstrato para que você possa considerar essa duplicação no teste. Tudo isso cheira! Eu recomendo dar uma outra olhada no porquê de você estar usando classes abstratas em primeiro lugar e ver se outra coisa funcionaria melhor.
Nigel Thorne

5
a próxima resposta exagerada é muito melhor.
Martin Spamer

22
@ MartiSpamer: Eu não diria que esta resposta está supervalorizada porque foi escrita muito antes (2 anos) do que a resposta que você considera melhor abaixo. Vamos apenas encorajar Patrick, porque no contexto de quando ele postou esta resposta, foi ótimo. Vamos encorajar um ao outro. Cheers
Marvin Thobejane

449

Existem duas maneiras pelas quais as classes base abstratas são usadas.

  1. Você está especializando seu objeto abstrato, mas todos os clientes usarão a classe derivada por meio de sua interface base.

  2. Você está usando uma classe base abstrata para fatorar a duplicação nos objetos em seu design, e os clientes usam as implementações concretas por meio de suas próprias interfaces.


Solução para 1 - Padrão de Estratégia

Opção 1

Se você tiver a primeira situação, realmente terá uma interface definida pelos métodos virtuais na classe abstrata que suas classes derivadas estão implementando.

Você deve considerar fazer disso uma interface real, alterar sua classe abstrata para ser concreta e tomar uma instância dessa interface em seu construtor. Suas classes derivadas tornam-se implementações dessa nova interface.

IMotor

Isso significa que agora você pode testar sua classe abstrata abstrata usando uma instância simulada da nova interface e cada nova implementação através da interface agora pública. Tudo é simples e testável.


Solução para 2

Se você tiver a segunda situação, sua classe abstrata estará trabalhando como uma classe auxiliar.

AbstractHelper

Dê uma olhada na funcionalidade que ela contém. Veja se alguma delas pode ser empurrada para os objetos que estão sendo manipulados para minimizar essa duplicação. Se você ainda tiver alguma coisa, tente torná-la uma classe auxiliar que sua implementação concreta leva no construtor e remova a classe base.

Motor Helper

Isso novamente leva a classes concretas que são simples e facilmente testáveis.


Como uma regra

Favorecer redes complexas de objetos simples em detrimento de uma rede simples de objetos complexos.

A chave para código testável extensível são pequenos blocos de construção e fiação independente.


Atualizado: Como lidar com misturas de ambos?

É possível ter uma classe base executando essas duas funções ... ou seja: possui uma interface pública e métodos auxiliares protegidos. Se for esse o caso, você pode fatorar os métodos auxiliares em uma classe (cenário2) e converter a árvore de herança em um padrão de estratégia.

Se você achar que tem alguns métodos que sua classe base implementa diretamente e outros são virtuais, ainda é possível converter a árvore de herança em um padrão de estratégia, mas eu também consideraria um bom indicador de que as responsabilidades não estão alinhadas corretamente e podem precisa de refatoração.


Atualização 2: Classes abstratas como um trampolim (12/06/2014)

Eu tive uma situação outro dia em que usei abstract, então gostaria de explorar o porquê.

Temos um formato padrão para nossos arquivos de configuração. Esta ferramenta específica possui 3 arquivos de configuração, todos nesse formato. Eu queria uma classe fortemente tipada para cada arquivo de configuração, para que, através da injeção de dependência, uma classe pudesse solicitar as configurações de que se importava.

Eu implementei isso tendo uma classe base abstrata que sabe analisar os formatos dos arquivos de configurações e as classes derivadas que expunham esses mesmos métodos, mas encapsulavam o local do arquivo de configurações.

Eu poderia ter escrito um "SettingsFileParser" que as 3 classes agruparam e depois delegado à classe base para expor os métodos de acesso a dados. Eu escolhi não fazer isso ainda, pois isso levaria a 3 classes derivadas com mais delegação código de do que qualquer outra coisa.

No entanto ... conforme esse código evolui e os consumidores de cada uma dessas classes de configurações ficam mais claros. Cada usuário de configurações solicitará algumas configurações e as transformará de alguma maneira (como as configurações são texto, elas podem ser agrupadas em objetos para convertê-las em números etc.). Quando isso acontece, vou começar a extrair essa lógica para métodos de manipulação de dados e empurrá-los de volta para as classes de configurações fortemente tipadas. Isso levará a uma interface de nível superior para cada conjunto de configurações, que eventualmente não está mais ciente de que está lidando com 'configurações'.

Nesse ponto, as classes de configurações fortemente tipadas não precisarão mais dos métodos "getter" que expõem a implementação subjacente das "configurações".

Nesse ponto, eu não gostaria mais que sua interface pública incluísse os métodos do acessador de configurações; então vou mudar essa classe para encapsular uma classe de analisador de configurações em vez de derivar dela.

A classe Abstract é, portanto: uma maneira de evitar o código de delegação no momento e um marcador no código para me lembrar de mudar o design mais tarde. Talvez eu nunca chegue a isso, então pode demorar um pouco ... apenas o código pode dizer.

Acho que isso é verdade com qualquer regra ... como "sem métodos estáticos" ou "sem métodos privados". Eles indicam um cheiro no código ... e isso é bom. Mantém você procurando a abstração que você perdeu ... e permite que você continue agregando valor ao seu cliente nesse meio tempo.

Eu imagino regras como esta definindo uma paisagem, onde o código sustentável vive nos vales. À medida que você adiciona um novo comportamento, é como chover no seu código. Inicialmente, você o coloca onde quer que ele apareça. Depois, refatora para permitir que as forças do bom design promovam o comportamento até que tudo acabe nos vales.


18
Esta é uma ótima resposta. Muito melhor do que o melhor classificado. Mas então eu acho que somente aqueles que realmente quero escrever código testável apreciaria .. :)
MalcomTucker

22
Não consigo superar o quão boa é essa resposta. Isso mudou totalmente minha maneira de pensar sobre aulas de abstrato. Obrigado Nigel.
MalcomTucker

4
Oh não .. outro princípio que eu tenho que repensar! Graças (ambos sarcasticamente, por agora, e não sarcasticamente por uma vez eu ter assimilado e se sentir como um programador melhor)
Martin Lyne

11
Boa resposta. Definitivamente, algo para se pensar ... mas o que você está dizendo basicamente não se resume a não usar classes abstratas?
Brianestey

32
+1 apenas para a regra "Favorecer redes complexas de objetos simples em detrimento de uma rede simples de objetos complexos".
David Glass

12

O que faço para classes abstratas e interfaces é o seguinte: Escrevo um teste, que usa o objeto como ele é concreto. Mas a variável do tipo X (X é a classe abstrata) não está definida no teste. Essa classe de teste não é adicionada ao conjunto de testes, mas suas subclasses, que possuem um método de configuração que define a variável para uma implementação concreta do X. Dessa forma, não duplico o código de teste. As subclasses do teste não usado podem adicionar mais métodos de teste, se necessário.


isso não causa problemas de conversão na subclasse? se X possui o método ae Y herda o X, mas também possui o método b. Quando você subclasse sua classe de teste, não precisa converter sua variável abstrata em um Y para executar testes em b?
Johnno Nolan

8

Para fazer um teste de unidade especificamente na classe abstrata, você deve derivá-lo para fins de teste, teste resultados base.method () e comportamento pretendido ao herdar.

Você testa um método chamando-o, então testa uma classe abstrata implementando-a ...


8

Se a sua classe abstrata contiver uma funcionalidade concreta que tenha valor comercial, normalmente eu a testarei diretamente criando um duplo de teste que oculte os dados abstratos ou usando uma estrutura de simulação para fazer isso por mim. Qual eu escolho depende muito se eu preciso escrever implementações específicas para testes dos métodos abstratos ou não.

O cenário mais comum em que preciso fazer isso é quando estou usando o padrão de método de modelo , como quando estou criando algum tipo de estrutura extensível que será usada por terceiros. Nesse caso, a classe abstrata é o que define o algoritmo que eu quero testar, por isso faz mais sentido testar a base abstrata do que uma implementação específica.

No entanto, acho importante que esses testes se concentrem apenas nas implementações concretas da lógica real dos negócios ; você não deve realizar os detalhes da implementação do teste de unidade da classe abstrata, pois acabará com testes frágeis.


6

Uma maneira é escrever um caso de teste abstrato que corresponda à sua classe abstrata e, em seguida, escrever casos de teste concretos que subclasses seu caso de teste abstrato. faça isso para cada subclasse concreta de sua classe abstrata original (ou seja, sua hierarquia de casos de teste reflete sua hierarquia de classes). consulte Testar uma interface no livro de receitas de junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

consulte também Superclasse do Testcase nos padrões xUnit: http://xunitpatterns.com/Testcase%20Superclass.html


4

Eu argumentaria contra testes "abstratos". Eu acho que um teste é uma idéia concreta e não tem uma abstração. Se você tiver elementos comuns, coloque-os em métodos ou classes auxiliares para que todos possam usar.

Quanto ao teste de uma classe de teste abstrata, lembre-se de perguntar o que está testando. Existem várias abordagens e você deve descobrir o que funciona no seu cenário. Você está tentando testar um novo método na sua subclasse? Em seguida, faça com que seus testes interajam apenas com esse método. Você está testando os métodos em sua classe base? Em seguida, provavelmente tenha um equipamento separado apenas para essa classe e teste cada método individualmente com quantos testes forem necessários.


Eu não queria testar novamente o código que já havia testado, por isso estava seguindo o caminho abstrato do caso de teste. Estou tentando testar todos os métodos concretos da minha aula abstrata em um só lugar.
Paul Whelan

7
Não concordo em extrair elementos comuns para classes auxiliares, pelo menos em alguns casos (muitos?). Se uma classe abstrata contém alguma funcionalidade concreta, acho perfeitamente aceitável testar essa funcionalidade diretamente.
Seth Petry-Johnson

4

Este é o padrão que geralmente sigo ao configurar um chicote para testar uma classe abstrata:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

E a versão que eu uso em teste:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

Se os métodos abstratos são chamados quando eu não espero, os testes falham. Ao organizar os testes, posso facilmente esboçar os métodos abstratos com lambdas que executam afirmações, lançam exceções, retornam valores diferentes etc.


3

Se os métodos concretos invocam qualquer um dos métodos abstratos em que a estratégia não funcionará, e você deseja testar o comportamento de cada classe filho separadamente. Caso contrário, estendê-lo e remover os métodos abstratos, como você descreveu, deve ser bom, novamente desde que os métodos concretos da classe abstrata sejam dissociados das classes filho.


2

Suponho que você queira testar a funcionalidade básica de uma classe abstrata ... Mas você provavelmente seria melhor estendendo a classe sem substituir nenhum método, e zombando com o mínimo esforço dos métodos abstratos.


2

Uma das principais motivações para o uso de uma classe abstrata é ativar o polimorfismo em seu aplicativo - ou seja: você pode substituir uma versão diferente em tempo de execução. De fato, isso é quase o mesmo que usar uma interface, exceto que a classe abstrata fornece algum encanamento comum, geralmente chamado de padrão de modelo .

De uma perspectiva de teste de unidade, há duas coisas a considerar:

  1. Interação da sua classe abstrata com as classes relacionadas . O uso de uma estrutura de teste simulada é ideal para esse cenário, pois mostra que sua classe abstrata funciona bem com outras pessoas.

  2. Funcionalidade de classes derivadas . Se você tiver uma lógica personalizada que você escreveu para suas classes derivadas, teste essas classes isoladamente.

edit: RhinoMocks é uma incrível estrutura de testes simulados que pode gerar objetos simulados em tempo de execução, derivando dinamicamente de sua classe. Essa abordagem pode economizar inúmeras horas de aulas derivadas de codificação manual.


2

Primeiro, se a classe abstrata continha algum método concreto, acho que você deveria fazer isso, considerando este exemplo

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

1

Se uma classe abstrata for apropriada para sua implementação, teste (como sugerido acima) uma classe concreta derivada. Suas suposições estão corretas.

Para evitar futuras confusões, saiba que essa classe de teste concreta não é uma farsa , mas uma farsa .

Em termos estritos, um mock é definido pelas seguintes características:

  • Um mock é usado no lugar de toda e qualquer dependência da classe de assunto que está sendo testada.
  • Um mock é uma pseudo-implementação de uma interface (você pode se lembrar que, como regra geral, as dependências devem ser declaradas como interfaces; a testabilidade é uma das principais razões para isso)
  • Os comportamentos dos membros da interface da simulação - sejam métodos ou propriedades - são fornecidos no momento do teste (novamente, usando uma estrutura de simulação). Dessa forma, você evita o acoplamento da implementação que está sendo testada com a implementação de suas dependências (que devem ter seus próprios testes discretos).

1

Após a resposta de @ patrick-desjardins, implementei abstract e sua classe de implementação @Test, da seguinte forma:

Classe abstrata - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

Como as classes Abstratas não podem ser instanciadas, mas podem ser subclassificadas , a classe concreta DEF.java é a seguinte:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

Classe @Test para testar o método abstrato e não-abstrato:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.