Por que o fixtureSetup do jUnit deve ser estático?

109

Eu marquei um método com a anotação @BeforeClass do jUnit e recebi esta exceção dizendo que ele deve ser estático. Qual é o motivo? Isso força todo o meu init a estar em campos estáticos, sem uma boa razão pelo que vejo.

Em .Net (NUnit), esse não é o caso.

Editar - o fato de que um método anotado com @BeforeClass é executado apenas uma vez não tem nada a ver com ser um método estático - pode-se ter um método não estático executado apenas uma vez (como em NUnit).

ripper234
fonte

Respostas:

122

JUnit sempre cria uma instância da classe de teste para cada método @Test. Esta é uma decisão de design fundamental para tornar mais fácil escrever testes sem efeitos colaterais. Bons testes não têm dependências de ordem de execução (consulte FIRST ) e criar novas instâncias da classe de teste e suas variáveis ​​de instância para cada teste é crucial para conseguir isso. Algumas estruturas de teste reutilizam a mesma instância de classe de teste para todos os testes, o que leva a mais possibilidades de criar acidentalmente efeitos colaterais entre os testes.

E como cada método de teste tem sua própria instância, não faz sentido que os métodos @ BeforeClass / @ AfterClass sejam métodos de instância. Caso contrário, em qual das instâncias da classe de teste os métodos devem ser chamados? Se fosse possível para os métodos @ BeforeClass / @ AfterClass referenciarem variáveis ​​de instância, então apenas um dos métodos @Test teria acesso a essas mesmas variáveis ​​de instância - o resto teria as variáveis ​​de instância em seus valores padrão - e o @ O método de teste seria selecionado aleatoriamente, porque a ordem dos métodos no arquivo .class não é especificada / dependente do compilador (IIRC, a API de reflexão do Java retorna os métodos na mesma ordem em que são declarados no arquivo .class, embora também esse comportamento não é especificado - eu escrevi uma biblioteca para realmente classificá-las por seus números de linha).

Portanto, impor que esses métodos sejam estáticos é a única solução razoável.

Aqui está um exemplo:

public class ExampleTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("beforeClass");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("afterClass");
    }

    @Before
    public void before() {
        System.out.println(this + "\tbefore");
    }

    @After
    public void after() {
        System.out.println(this + "\tafter");
    }

    @Test
    public void test1() {
        System.out.println(this + "\ttest1");
    }

    @Test
    public void test2() {
        System.out.println(this + "\ttest2");
    }

    @Test
    public void test3() {
        System.out.println(this + "\ttest3");
    }
}

Quais impressões:

beforeClass
ExampleTest@3358fd70    before
ExampleTest@3358fd70    test1
ExampleTest@3358fd70    after
ExampleTest@6293068a    before
ExampleTest@6293068a    test2
ExampleTest@6293068a    after
ExampleTest@22928095    before
ExampleTest@22928095    test3
ExampleTest@22928095    after
afterClass

Como você pode ver, cada um dos testes é executado com sua própria instância. O que o JUnit faz é basicamente o mesmo:

ExampleTest.beforeClass();

ExampleTest t1 = new ExampleTest();
t1.before();
t1.test1();
t1.after();

ExampleTest t2 = new ExampleTest();
t2.before();
t2.test2();
t2.after();

ExampleTest t3 = new ExampleTest();
t3.before();
t3.test3();
t3.after();

ExampleTest.afterClass();
Esko Luontola
fonte
1
"Caso contrário, em qual das instâncias da classe de teste os métodos devem ser chamados?" - Na instância de teste que o teste JUnit em execução criou para executar os testes.
HDave
1
Nesse exemplo, ele criou três instâncias de teste. Não existe a instância de teste.
Esko Luontola
Sim - eu perdi isso no seu exemplo. Eu estava pensando mais sobre quando JUnit é invocado de um teste em execução ala Eclipse, ou Spring Test ou Maven. Nesses casos, há uma instância de uma classe de teste criada.
HDave 01 de
Não, o JUnit sempre cria várias instâncias da classe de teste, independentemente do que usamos para iniciar os testes. Somente se você tiver um Runner personalizado para uma classe de teste, algo diferente pode acontecer.
Esko Luontola
Embora eu entenda a decisão de design, acho que ela não leva em consideração as necessidades de negócios dos usuários. Então, no final, a decisão de design interno (que eu não deveria me importar tanto como um usuário assim que a biblioteca funcionasse bem) me força a fazer escolhas de design em meus testes que são práticas realmente ruins. Isso não é nada ágil: D
gicappa
43

A resposta curta é esta: não há um bom motivo para ser estático.

Na verdade, torná-lo estático causa todos os tipos de problemas se você usar o Junit para executar testes de integração DAO baseados em DBUnit. O requisito estático interfere na injeção de dependência, acesso ao contexto do aplicativo, manipulação de recursos, registro e qualquer coisa que dependa de "getClass".

HDave
fonte
4
Eu escrevi minha própria superclasse de caso de teste e uso as anotações do Spring @PostConstructpara configurar e @AfterClassderrubar, e ignoro as anotações estáticas do Junit. Para os testes DAO, escrevi minha própria TestCaseDataLoaderclasse, que invoco a partir desses métodos.
HDave 01 de
9
Essa é uma resposta terrível, claramente há uma razão para ser estática como a resposta aceita claramente indica. Você pode discordar da decisão de design, mas isso está longe de implicar que não há "um bom motivo" para a decisão.
Adam Parkin
8
É claro que os autores do JUnit tinham um motivo, estou dizendo que não é um bom motivo ... portanto, a fonte do OP (e 44 outras pessoas) foi mistificada. Teria sido trivial usar métodos de instância e fazer com que os executores de teste empregassem uma convenção para chamá-los. No final, é o que todos fazem para contornar essa limitação - role seu próprio runner ou role sua própria classe de teste.
HDave 01 de
1
@HDave, acho que sua solução com @PostConstructe @AfterClassapenas se comporta da mesma forma que @Beforee @After. Na verdade, seus métodos serão chamados para cada método de teste e não uma vez para a classe inteira (como Esko Luontola afirma em sua resposta, uma instância de classe é criada para cada método de teste). Não consigo ver a utilidade da sua solução, então (a menos que eu perca algo)
magnum87
1
Está funcionando corretamente há 5 anos, então estou pensando que minha solução funciona.
HDave
13

A documentação do JUnit parece escassa, mas suponho: talvez o JUnit crie uma nova instância de sua classe de teste antes de executar cada caso de teste, então a única maneira de seu estado de "fixação" persistir entre as execuções é torná-lo estático, o que pode ser reforçado certificando-se de que seu fixtureSetup (método @BeforeClass) é estático.

Blair Conrad
fonte
2
Não apenas talvez, mas JUnit definitivamente cria uma nova instância de um caso de teste. Portanto, esta é a única razão.
guerda
Esta é a única razão que eles têm, mas na verdade o executor Junit poderia fazer o trabalho de executar métodos BeforeTests e AfterTests da mesma forma que o testng.
HDave
O TestNG cria uma instância da classe de teste e a compartilha com todos os testes da classe? Isso o torna mais vulnerável aos efeitos colaterais entre os testes.
Esko Luontola
3

Embora isso não responda à pergunta original. Ele responderá ao acompanhamento óbvio. Como criar uma regra que funcione antes e depois de uma aula e antes e depois de um teste.

Para conseguir isso, você pode usar este padrão:

@ClassRule
public static JPAConnection jpaConnection = JPAConnection.forUITest("my-persistence-unit");

@Rule
public JPAConnection.EntityManager entityManager = jpaConnection.getEntityManager();

Em before (Class), o JPAConnection cria a conexão uma vez depois de (Class) ele a fecha.

getEntityMangerretorna uma classe interna JPAConnectionque implementa o EntityManager do jpa e pode acessar a conexão dentro do jpaConnection. Em antes (teste), ele começa uma transação após (teste) ele faz o rollback novamente.

Isso não é thread-safe, mas pode ser feito para ser assim.

Código selecionado de JPAConnection.class

package com.triodos.general.junit;

import com.triodos.log.Logger;
import org.jetbrains.annotations.NotNull;
import org.junit.rules.ExternalResource;

import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.Metamodel;
import java.util.HashMap;
import java.util.Map;

import static com.google.common.base.Preconditions.checkState;
import static com.triodos.dbconn.DB2DriverManager.DRIVERNAME_TYPE4;
import static com.triodos.dbconn.UnitTestProperties.getDatabaseConnectionProperties;
import static com.triodos.dbconn.UnitTestProperties.getPassword;
import static com.triodos.dbconn.UnitTestProperties.getUsername;
import static java.lang.String.valueOf;
import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

public final class JPAConnectionExample extends ExternalResource {

  private static final Logger LOG = Logger.getLogger(JPAConnectionExample.class);

  @NotNull
  public static JPAConnectionExample forUITest(String persistenceUnitName) {
    return new JPAConnectionExample(persistenceUnitName)
        .setManualEntityManager();
  }

  private final String persistenceUnitName;
  private EntityManagerFactory entityManagerFactory;
  private javax.persistence.EntityManager jpaEntityManager = null;
  private EntityManager entityManager;

  private JPAConnectionExample(String persistenceUnitName) {
    this.persistenceUnitName = persistenceUnitName;
  }

  @NotNull
  private JPAConnectionExample setEntityManager(EntityManager entityManager) {
    this.entityManager = entityManager;
    return this;
  }

  @NotNull
  private JPAConnectionExample setManualEntityManager() {
    return setEntityManager(new RollBackAfterTestEntityManager());
  }


  @Override
  protected void before() {
    entityManagerFactory = Persistence.createEntityManagerFactory(persistenceUnitName, createEntityManagerProperties());
    jpaEntityManager = entityManagerFactory.createEntityManager();
  }

  @Override
  protected void after() {

    if (jpaEntityManager.getTransaction().isActive()) {
      jpaEntityManager.getTransaction().rollback();
    }

    if(jpaEntityManager.isOpen()) {
      jpaEntityManager.close();
    }
    // Free for garbage collection as an instance
    // of EntityManager may be assigned to a static variable
    jpaEntityManager = null;

    entityManagerFactory.close();
    // Free for garbage collection as an instance
    // of JPAConnection may be assigned to a static variable
    entityManagerFactory = null;
  }

  private Map<String,String> createEntityManagerProperties(){
    Map<String, String> properties = new HashMap<>();
    properties.put("javax.persistence.jdbc.url", getDatabaseConnectionProperties().getURL());
    properties.put("javax.persistence.jtaDataSource", null);
    properties.put("hibernate.connection.isolation", valueOf(TRANSACTION_READ_UNCOMMITTED));
    properties.put("hibernate.connection.username", getUsername());
    properties.put("hibernate.connection.password", getPassword());
    properties.put("hibernate.connection.driver_class", DRIVERNAME_TYPE4);
    properties.put("org.hibernate.readOnly", valueOf(true));

    return properties;
  }

  @NotNull
  public EntityManager getEntityManager(){
    checkState(entityManager != null);
    return entityManager;
  }


  private final class RollBackAfterTestEntityManager extends EntityManager {

    @Override
    protected void before() throws Throwable {
      super.before();
      jpaEntityManager.getTransaction().begin();
    }

    @Override
    protected void after() {
      super.after();

      if (jpaEntityManager.getTransaction().isActive()) {
        jpaEntityManager.getTransaction().rollback();
      }
    }
  }

  public abstract class EntityManager extends ExternalResource implements javax.persistence.EntityManager {

    @Override
    protected void before() throws Throwable {
      checkState(jpaEntityManager != null, "JPAConnection was not initialized. Is it a @ClassRule? Did the test runner invoke the rule?");

      // Safety-close, if failed to close in setup
      if (jpaEntityManager.getTransaction().isActive()) {
        jpaEntityManager.getTransaction().rollback();
        LOG.error("EntityManager encountered an open transaction at the start of a test. Transaction has been closed but should have been closed in the setup method");
      }
    }

    @Override
    protected void after() {
      checkState(jpaEntityManager != null, "JPAConnection was not initialized. Is it a @ClassRule? Did the test runner invoke the rule?");
    }

    @Override
    public final void persist(Object entity) {
      jpaEntityManager.persist(entity);
    }

    @Override
    public final <T> T merge(T entity) {
      return jpaEntityManager.merge(entity);
    }

    @Override
    public final void remove(Object entity) {
      jpaEntityManager.remove(entity);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey) {
      return jpaEntityManager.find(entityClass, primaryKey);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties) {
      return jpaEntityManager.find(entityClass, primaryKey, properties);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode) {
      return jpaEntityManager.find(entityClass, primaryKey, lockMode);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode, Map<String, Object> properties) {
      return jpaEntityManager.find(entityClass, primaryKey, lockMode, properties);
    }

    @Override
    public final <T> T getReference(Class<T> entityClass, Object primaryKey) {
      return jpaEntityManager.getReference(entityClass, primaryKey);
    }

    @Override
    public final void flush() {
      jpaEntityManager.flush();
    }

    @Override
    public final void setFlushMode(FlushModeType flushMode) {
      jpaEntityManager.setFlushMode(flushMode);
    }

    @Override
    public final FlushModeType getFlushMode() {
      return jpaEntityManager.getFlushMode();
    }

    @Override
    public final void lock(Object entity, LockModeType lockMode) {
      jpaEntityManager.lock(entity, lockMode);
    }

    @Override
    public final void lock(Object entity, LockModeType lockMode, Map<String, Object> properties) {
      jpaEntityManager.lock(entity, lockMode, properties);
    }

    @Override
    public final void refresh(Object entity) {
      jpaEntityManager.refresh(entity);
    }

    @Override
    public final void refresh(Object entity, Map<String, Object> properties) {
      jpaEntityManager.refresh(entity, properties);
    }

    @Override
    public final void refresh(Object entity, LockModeType lockMode) {
      jpaEntityManager.refresh(entity, lockMode);
    }

    @Override
    public final void refresh(Object entity, LockModeType lockMode, Map<String, Object> properties) {
      jpaEntityManager.refresh(entity, lockMode, properties);
    }

    @Override
    public final void clear() {
      jpaEntityManager.clear();
    }

    @Override
    public final void detach(Object entity) {
      jpaEntityManager.detach(entity);
    }

    @Override
    public final boolean contains(Object entity) {
      return jpaEntityManager.contains(entity);
    }

    @Override
    public final LockModeType getLockMode(Object entity) {
      return jpaEntityManager.getLockMode(entity);
    }

    @Override
    public final void setProperty(String propertyName, Object value) {
      jpaEntityManager.setProperty(propertyName, value);
    }

    @Override
    public final Map<String, Object> getProperties() {
      return jpaEntityManager.getProperties();
    }

    @Override
    public final Query createQuery(String qlString) {
      return jpaEntityManager.createQuery(qlString);
    }

    @Override
    public final <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery) {
      return jpaEntityManager.createQuery(criteriaQuery);
    }

    @Override
    public final <T> TypedQuery<T> createQuery(String qlString, Class<T> resultClass) {
      return jpaEntityManager.createQuery(qlString, resultClass);
    }

    @Override
    public final Query createNamedQuery(String name) {
      return jpaEntityManager.createNamedQuery(name);
    }

    @Override
    public final <T> TypedQuery<T> createNamedQuery(String name, Class<T> resultClass) {
      return jpaEntityManager.createNamedQuery(name, resultClass);
    }

    @Override
    public final Query createNativeQuery(String sqlString) {
      return jpaEntityManager.createNativeQuery(sqlString);
    }

    @Override
    public final Query createNativeQuery(String sqlString, Class resultClass) {
      return jpaEntityManager.createNativeQuery(sqlString, resultClass);
    }

    @Override
    public final Query createNativeQuery(String sqlString, String resultSetMapping) {
      return jpaEntityManager.createNativeQuery(sqlString, resultSetMapping);
    }

    @Override
    public final void joinTransaction() {
      jpaEntityManager.joinTransaction();
    }

    @Override
    public final <T> T unwrap(Class<T> cls) {
      return jpaEntityManager.unwrap(cls);
    }

    @Override
    public final Object getDelegate() {
      return jpaEntityManager.getDelegate();
    }

    @Override
    public final void close() {
      jpaEntityManager.close();
    }

    @Override
    public final boolean isOpen() {
      return jpaEntityManager.isOpen();
    }

    @Override
    public final EntityTransaction getTransaction() {
      return jpaEntityManager.getTransaction();
    }

    @Override
    public final EntityManagerFactory getEntityManagerFactory() {
      return jpaEntityManager.getEntityManagerFactory();
    }

    @Override
    public final CriteriaBuilder getCriteriaBuilder() {
      return jpaEntityManager.getCriteriaBuilder();
    }

    @Override
    public final Metamodel getMetamodel() {
      return jpaEntityManager.getMetamodel();
    }
  }
}
MP Korstanje
fonte
2

Parece que o JUnit cria uma nova instância da classe de teste para cada método de teste. Experimente este código

public class TestJunit
{

    int count = 0;

    @Test
    public void testInc1(){
        System.out.println(count++);
    }

    @Test
    public void testInc2(){
        System.out.println(count++);
    }

    @Test
    public void testInc3(){
        System.out.println(count++);
    }
}

A saída é 0 0 0

Isso significa que se o método @BeforeClass não for estático, ele deverá ser executado antes de cada método de teste e não haverá como diferenciar entre a semântica de @Before e @BeforeClass

usuário aleatório
fonte
Não apenas parece assim, é assim. A pergunta vem sendo feita há muitos anos, aqui está a resposta: martinfowler.com/bliki/JunitNewInstance.html
Paulo
1

existem dois tipos de anotações:

  • @BeforeClass (@AfterClass) chamado uma vez por classe de teste
  • @Before (e @After) chamado antes de cada teste

portanto, @BeforeClass deve ser declarado estático porque é chamado uma vez. Você também deve considerar que ser estático é a única maneira de garantir a propagação de "estado" adequada entre os testes (o modelo JUnit impõe uma instância de teste por @Test) e, uma vez que em Java apenas métodos estáticos podem acessar dados estáticos ... @BeforeClass e @ AfterClass pode ser aplicado apenas a métodos estáticos.

Este exemplo de teste deve esclarecer o uso de @BeforeClass vs @Before:

public class OrderTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("before class");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("after class");
    }

    @Before
    public void before() {
        System.out.println("before");
    }

    @After
    public void after() {
        System.out.println("after");
    }    

    @Test
    public void test1() {
        System.out.println("test 1");
    }

    @Test
    public void test2() {
        System.out.println("test 2");
    }
}

resultado:

------------- Saída padrão ---------------
antes da aula
antes
teste 1
depois de
antes
teste 2
depois de
depois da aula
------------- ---------------- ---------------
dfa
fonte
19
Acho sua resposta irrelevante. Eu conheço a semântica de BeforeClass e Before. Isso não explica por que tem que ser estático ...
ripper234
1
"Isso força todo o meu init a estar em membros estáticos, sem uma boa razão até onde eu vejo." Minha resposta deve mostrar que seu init também pode ser não estático usando @Before, em vez de @BeforeClass
dfa
2
Eu gostaria de fazer parte do init apenas uma vez, no início da aula, mas em variáveis ​​não estáticas.
ripper234
você não pode com JUnit, desculpe. Você deve usar uma variável estática, de jeito nenhum.
dfa
1
Se a inicialização for cara, você pode apenas manter uma variável de estado para registrar se você fez a inicialização e (verifique e opcionalmente) execute a inicialização em um método @Before ...
Blair Conrad
0

De acordo com o JUnit 5, parece que a filosofia de criar estritamente uma nova instância por método de teste foi um pouco afrouxada. Eles adicionaram uma anotação que irá instanciar uma classe de teste apenas uma vez. Esta anotação, portanto, também permite que os métodos anotados com @ BeforeAll / @ AfterAll (as substituições para @ BeforeClass / @ AfterClass) sejam não estáticos. Então, uma classe de teste como esta:

@TestInstance(Lifecycle.PER_CLASS)
class TestClass() {
    Object object;

    @BeforeAll
    void beforeAll() {
        object = new Object();
    }

    @Test
    void testOne() {
        System.out.println(object);
    }

    @Test
    void testTwo() {
        System.out.println(object);
    }
}

imprimiria:

java.lang.Object@799d4f69
java.lang.Object@799d4f69

Portanto, você pode realmente instanciar objetos uma vez por classe de teste. Claro, isso torna sua responsabilidade evitar a mutação de objetos que são instanciados dessa maneira.

EJJ
fonte
-11

Para resolver este problema, basta alterar o método

public void setUpBeforeClass 

para

public static void setUpBeforeClass()

e todos os que são definidos neste método para static.

sri
fonte
2
Isso não responde à pergunta de forma alguma.
rgargente