Prática recomendada: inicialize os campos da classe JUnit em setUp () ou na declaração?

120

Devo inicializar campos de classe em declarações como esta?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Ou em setUp () assim?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Costumo usar o primeiro formulário porque é mais conciso e me permite usar os campos finais. Se eu não precisar usar o método setUp () para instalação, ainda devo usá-lo e por quê?

Esclarecimento: O JUnit instancia a classe de teste uma vez por método de teste. Isso significa listque será criado uma vez por teste, independentemente de onde eu o declare. Isso também significa que não há dependências temporais entre os testes. Portanto, parece que não há vantagens em usar setUp (). No entanto, a FAQ do JUnit tem muitos exemplos que inicializam uma coleção vazia em setUp (), então acho que deve haver um motivo.

Craig P. Motlin
fonte
2
Cuidado que a resposta difere no JUnit 4 (inicialize na declaração) e no JUnit 3 (use setUp); essa é a raiz da confusão.
Nils von Barth
Veja também stackoverflow.com/questions/6094081/…
Grigory Kislin

Respostas:

99

Se você está se perguntando especificamente sobre os exemplos nas Perguntas frequentes da JUnit, como o modelo de teste básico , acho que a melhor prática a ser mostrada é que a classe em teste deve ser instanciada no seu método setUp (ou em um método de teste) .

Quando os exemplos JUnit criam um ArrayList no método setUp, todos eles testam o comportamento desse ArrayList, com casos como testIndexOutOfBoundException, testEmptyCollection e similares. A perspectiva é de alguém escrever uma classe e garantir que ela funcione corretamente.

Você provavelmente deve fazer o mesmo ao testar suas próprias classes: crie seu objeto em setUp ou em um método de teste, para que você possa obter uma saída razoável se a interromper mais tarde.

Por outro lado, se você usa uma classe de coleção Java (ou outra classe de biblioteca) no seu código de teste, provavelmente não é porque você deseja testá-lo - é apenas parte do equipamento de teste. Nesse caso, você pode assumir com segurança que ele funciona como pretendido, portanto, inicializá-lo na declaração não será um problema.

Pelo que vale, eu trabalho em uma base de código razoavelmente grande, com vários anos de idade e desenvolvida por TDD. Habitualmente, inicializamos as coisas em suas declarações no código de teste e, no ano e meio em que estive neste projeto, isso nunca causou um problema. Portanto, há pelo menos alguma evidência anedótica de que é uma coisa razoável a se fazer.

Moss Collum
fonte
45

Comecei a me cavar e encontrei uma vantagem em potencial do uso setUp(). Se alguma exceção for lançada durante a execução de setUp(), o JUnit imprimirá um rastreamento de pilha muito útil. Por outro lado, se uma exceção for lançada durante a construção do objeto, a mensagem de erro simplesmente diz que o JUnit não conseguiu instanciar o caso de teste e você não vê o número da linha onde ocorreu a falha, provavelmente porque o JUnit usa reflexão para instanciar o teste Aulas.

Nada disso se aplica ao exemplo de criação de uma coleção vazia, pois isso nunca será lançado, mas é uma vantagem do setUp()método.

Craig P. Motlin
fonte
18

Além da resposta de Alex B.

É necessário usar o método setUp para instanciar recursos em um determinado estado. Fazer isso no construtor não é apenas uma questão de tempos, mas, devido à maneira como o JUnit executa os testes, cada estado de teste será apagado após a execução de um.

O JUnit primeiro cria instâncias do testClass para cada método de teste e começa a executar os testes após a criação de cada instância. Antes de executar o método de teste, seu método de configuração é executado, no qual algum estado pode ser preparado.

Se o estado do banco de dados fosse criado no construtor, todas as instâncias instanciariam o estado db logo após o outro, antes de executar cada teste. A partir do segundo teste, os testes seriam executados com um estado sujo.

Ciclo de vida das JUnits:

  1. Crie uma instância de classe de teste diferente para cada método de teste
  2. Repita para cada instância da classe de teste: chame setup + chame o testmethod

Com alguns registros em um teste com dois métodos de teste, você obtém: (number é o hashcode)

  • Criando nova instância: 5718203
  • Criando nova instância: 5947506
  • Instalação: 5718203
  • TestOne: 5718203
  • Instalação: 5947506
  • TestTwo: 5947506
Jurgen Hannaert
fonte
3
Correto, mas fora de tópico. O banco de dados é essencialmente estado global. Este não é um problema que enfrento. Estou apenas preocupado com a velocidade de execução de testes adequadamente independentes.
22716 Craig P. Motlin
Essa ordem de inicialização é verdadeira apenas na JUnit 3, onde é um cuidado importante. No JUnit 4, as instâncias de teste são criadas preguiçosamente; portanto, a inicialização na declaração ou em um método de configuração ocorre no momento do teste. Também para a instalação de uma só vez, pode-se usar @BeforeClassem JUnit 4.
Nils von Barth
11

Na JUnit 4:

  • Para a classe em teste , inicialize em um @Beforemétodo, para detectar falhas.
  • Para outras classes , inicialize na declaração ...
    • ... por finalquestões de brevidade e para marcar os campos exatamente como indicado na pergunta,
    • ... a menos que seja uma inicialização complexa que poderia falhar, nesse caso @Before, usar , para detectar falhas.
  • Para o estado global (especialmente inicialização lenta , como um banco de dados), use @BeforeClass, mas tenha cuidado com as dependências entre os testes.
  • A inicialização de um objeto usado em um único teste deve, obviamente, ser feita no próprio método de teste.

A inicialização de um @Beforemétodo ou método de teste permite obter melhores relatórios de erros sobre falhas. Isso é especialmente útil para instanciar a classe em teste (que você pode interromper), mas também é útil para chamar sistemas externos, como acesso ao sistema de arquivos ("arquivo não encontrado") ou conectar-se a um banco de dados ("conexão recusada").

É aceitável ter um padrão simples e sempre usar @Before(erros claros, mas detalhados) ou sempre inicializar na declaração (conciso, mas gera erros confusos), já que regras de codificação complexas são difíceis de seguir, e isso não é grande coisa.

A inicialização de in setUpé uma relíquia do JUnit 3, onde todas as instâncias de teste foram inicializadas rapidamente, o que causa problemas (velocidade, memória, esgotamento de recursos) se você fizer uma inicialização cara. Portanto, a melhor prática era fazer uma inicialização cara setUp, que só era executada quando o teste foi executado. Isso não se aplica mais, portanto, é muito menos necessário usá-lo setUp.

Isso resume várias outras respostas que enterram o lede, especialmente por Craig P. Motlin (pergunta em si e resposta automática), Moss Collum (classe em teste) e dsaff.

Nils von Barth
fonte
7

No JUnit 3, seus inicializadores de campo serão executados uma vez por método de teste antes de qualquer teste ser executado . Desde que seus valores de campo sejam pequenos na memória, demore pouco tempo de configuração e não afete o estado global, o uso de inicializadores de campo é tecnicamente adequado. No entanto, se isso não acontecer, você pode acabar gastando muita memória ou tempo configurando seus campos antes da execução do primeiro teste e possivelmente até ficando sem memória. Por esse motivo, muitos desenvolvedores sempre definem valores de campo no método setUp (), onde é sempre seguro, mesmo quando não é estritamente necessário.

Observe que no JUnit 4, a inicialização do objeto de teste acontece logo antes da execução do teste, portanto, o uso de inicializadores de campo é um estilo mais seguro e recomendado.

dsaff
fonte
Interessante. Portanto, o comportamento que você descreveu primeiro se aplica apenas à JUnit 3?
Craig P. Motlin
6

No seu caso (criando uma lista), não há diferença na prática. Mas geralmente é melhor usar setUp (), porque isso ajudará a Junit a relatar exceções corretamente. Se ocorrer uma exceção no construtor / inicializador de um Teste, isso será uma falha no teste . No entanto, se ocorrer uma exceção durante a instalação, é natural pensar nisso como um problema na configuração do teste, e o junit o reporta adequadamente.

amit
fonte
1
bem dito. Apenas se acostume a instanciar sempre em setUp () e você tem uma pergunta a menos para se preocupar - por exemplo, onde devo instanciar meu fooBar, onde minha coleção. É um tipo de padrão de codificação que você só precisa seguir. Beneficia você não com listas, mas com outras instanciações.
Olaf Kock
@Olaf Obrigado pela informação sobre o padrão de codificação, eu não tinha pensado nisso. Eu tendem a concordar com a idéia de Moss Collum de um padrão de codificação mais.
21716 Craig P. Motlin
5

Prefiro a legibilidade primeiro, que na maioria das vezes não usa o método de instalação. Faço uma exceção quando uma operação de configuração básica demora muito tempo e é repetida em cada teste.
Nesse ponto, movo essa funcionalidade para um método de configuração usando a @BeforeClassanotação (otimizar posteriormente).

Exemplo de otimização usando o @BeforeClassmétodo de instalação: Eu uso o dbunit para alguns testes funcionais do banco de dados. O método de configuração é responsável por colocar o banco de dados em um estado conhecido (muito lento ... 30 segundos - 2 minutos, dependendo da quantidade de dados). Carrego esses dados no método de instalação anotado @BeforeClasse, em seguida, executo de 10 a 20 testes no mesmo conjunto de dados, em vez de recarregar / inicializar o banco de dados dentro de cada teste.

O uso do Junit 3.8 (estendendo o TestCase como mostrado no seu exemplo) requer a criação de um pouco mais de código do que apenas a adição de uma anotação, mas a "executar uma vez antes da configuração da classe" ainda é possível.

Alex B
fonte
1
+1 porque também prefiro legibilidade. No entanto, não estou convencido de que a segunda maneira seja uma otimização.
22716 Craig P. Motlin
@Motlin Adicionei o exemplo do dbunit para esclarecer como você pode otimizar com a instalação.
Alex B
O banco de dados é essencialmente estado global. Portanto, mover a configuração do banco de dados para setUp () não é uma otimização, é necessário que os testes sejam concluídos corretamente.
22730 Craig P. Motlin
@ Alex B: Como disse Motlin, isso não é uma otimização. Você está apenas mudando onde no código a inicialização é feita, mas não quantas vezes nem com que rapidez.
226 Eddie
Pretendia sugerir o uso da anotação "@BeforeClass". Editando o exemplo para esclarecer.
227 Alex B Alex
2

Como cada teste é executado de forma independente, com uma nova instância do objeto, não faz muito sentido que o objeto Teste tenha qualquer estado interno, exceto aquele compartilhado entre setUp()e um teste individual e tearDown(). Esse é um dos motivos (além dos motivos apresentados por outros): é bom usar o setUp()método.

Nota: É uma má idéia para um objeto de teste JUnit manter o estado estático! Se você usar variável estática em seus testes para outras finalidades que não sejam de rastreamento ou diagnóstico, estará invalidando parte do objetivo do JUnit, que é o de que os testes possam (pode) ser executados em qualquer ordem, cada teste executando com um estado fresco e limpo.

As vantagens de usar setUp()é que você não precisa recortar e colar o código de inicialização em todos os métodos de teste e que não possui código de configuração de teste no construtor. No seu caso, há pouca diferença. Apenas a criação de uma lista vazia pode ser feita com segurança, como você a mostra ou no construtor, pois é uma inicialização trivial. No entanto, como você e outras pessoas apontaram, qualquer coisa que possa gerar um Exceptiondeve ser feita setUp()para que você obtenha o despejo da pilha de diagnóstico se ele falhar.

No seu caso, onde você está apenas criando uma lista vazia, eu faria da mesma maneira que você está sugerindo: Atribua a nova lista no ponto da declaração. Especialmente porque dessa maneira você tem a opção de marcá-la finalse isso fizer sentido para a sua classe de teste.

Eddie
fonte
1
+1 porque você é a primeira pessoa a realmente apoiar a inicialização da lista durante a construção do objeto para marcá-la como final. O material sobre variáveis ​​estáticas está fora de tópico para a pergunta.
21716 Craig P. Motlin
@ Motlin: verdade, o material sobre variáveis ​​estáticas é um pouco fora de tópico. Não sei por que acrescentei isso, mas parecia apropriado na época uma extensão do que eu estava dizendo no primeiro parágrafo.
9119 Eddie
A vantagem de finalé mencionada na pergunta embora.
Nils von Barth
0
  • Os valores constantes (usados ​​em equipamentos ou afirmações) devem ser inicializados em suas declarações e final(como nunca mudam)

  • o objeto em teste deve ser inicializado no método de configuração, pois podemos definir as coisas. É claro que não podemos definir algo agora, mas podemos defini-lo mais tarde. Instanciar no método init facilitaria as alterações.

  • As dependências do objeto em teste, se elas forem zombadas, nem devem ser instanciadas por você: hoje, as estruturas de simulação podem instancia-lo por reflexão.

Um teste sem dependência para zombar pode parecer com:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

Um teste com dependências para isolar pode se parecer com:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

    @Test
    public void populateList()
         ...
    }
}
davidxxx
fonte