¿Por qué el fixtureSetup de jUnit debe ser estático?

109

Marqué un método con la anotación @BeforeClass de jUnit y obtuve esta excepción que decía que debe ser estático. ¿Cuál es la razón fundamental? Esto obliga a todos mis init a estar en campos estáticos, sin una buena razón por lo que veo.

En .Net (NUnit), este no es el caso.

Editar : el hecho de que un método anotado con @BeforeClass se ejecute solo una vez no tiene nada que ver con que sea un método estático; uno puede hacer que un método no estático se ejecute solo una vez (como en NUnit).

destripador234
fuente

Respuestas:

122

JUnit siempre crea una instancia de la clase de prueba para cada método @Test. Esta es una decisión de diseño fundamental para facilitar la escritura de pruebas sin efectos secundarios. Las buenas pruebas no tienen dependencias de orden de ejecución (consulte FIRST ) y la creación de nuevas instancias de la clase de prueba y sus variables de instancia para cada prueba es crucial para lograrlo. Algunos marcos de prueba reutilizan la misma instancia de clase de prueba para todas las pruebas, lo que genera más posibilidades de crear accidentalmente efectos secundarios entre pruebas.

Y como cada método de prueba tiene su propia instancia, no tiene sentido que los métodos @ BeforeClass / @ AfterClass sean métodos de instancia. De lo contrario, ¿en cuál de las instancias de la clase de prueba deberían llamarse los métodos? Si fuera posible que los métodos @ BeforeClass / @ AfterClass hicieran referencia a variables de instancia, entonces solo uno de los métodos @Test tendría acceso a esas mismas variables de instancia (el resto tendría las variables de instancia en sus valores predeterminados) y @ El método de prueba se seleccionaría aleatoriamente, porque el orden de los métodos en el archivo .class no está especificado / depende del compilador (IIRC, la API de reflexión de Java devuelve los métodos en el mismo orden en que se declaran en el archivo .class, aunque también ese comportamiento no está especificado - he escrito una biblioteca para clasificarlos por sus números de línea).

Por lo tanto, hacer que esos métodos sean estáticos es la única solución razonable.

Aquí hay un ejemplo:

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");
    }
}

Que imprime:

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 puede ver, cada una de las pruebas se ejecuta con su propia instancia. Lo que hace JUnit es básicamente lo mismo que esto:

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
fuente
1
"De lo contrario, ¿en cuál de las instancias de la clase de prueba deberían llamarse los métodos?" - En la instancia de prueba que creó la ejecución de prueba JUnit para ejecutar las pruebas.
HDave el
1
En ese ejemplo, creó tres instancias de prueba. No existe la instancia de prueba.
Esko Luontola
Sí, me perdí eso en tu ejemplo. Estaba pensando más en cuándo se invoca JUnit desde una prueba que ejecuta ala Eclipse, Spring Test o Maven. En esos casos, se crea una instancia de una clase de prueba.
HDave
No, JUnit siempre crea muchas instancias de la clase de prueba, independientemente de lo que usamos para iniciar las pruebas. Solo si tiene un Runner personalizado para una clase de prueba, podría suceder algo diferente.
Esko Luontola
Si bien entiendo la decisión de diseño, creo que no tiene en cuenta las necesidades comerciales de los usuarios. Entonces, al final, la decisión de diseño interno (que no debería importarme tanto como usuario tan pronto como la biblioteca funcione bien) me obliga a elegir opciones de diseño en mis pruebas que son realmente malas prácticas. Eso realmente no es ágil en absoluto: D
gicappa
43

La respuesta corta es la siguiente: no hay una buena razón para que sea estático.

De hecho, convertirlo en estático causa todo tipo de problemas si utiliza Junit para ejecutar pruebas de integración DAO basadas en DBUnit. El requisito estático interfiere con la inyección de dependencias, el acceso al contexto de la aplicación, el manejo de recursos, el registro y cualquier cosa que dependa de "getClass".

HDave
fuente
4
Escribí mi propia superclase de casos de prueba y utilicé las anotaciones de Spring @PostConstructpara configurar y @AfterClassderribar e ignoro las estáticas de Junit por completo. Para las pruebas DAO, luego escribí mi propia TestCaseDataLoaderclase que invoco a partir de estos métodos.
HDave
9
Esa es una respuesta terrible, claramente hay una razón para que sea estática, como lo indica claramente la respuesta aceptada. Puede que no esté de acuerdo con la decisión de diseño, pero eso está lejos de implicar que "no hay una buena razón" para la decisión.
Adam Parkin
8
Por supuesto, los autores de JUnit tenían una razón, digo que no es una buena razón ... por lo tanto, la fuente del OP (y otras 44 personas) está desconcertada. Habría sido trivial usar métodos de instancia y que los ejecutores de prueba emplearan una convención para llamarlos. Al final, eso es lo que todo el mundo hace para solucionar esta limitación, ya sea tirando tu propio corredor o tirando tu propia clase de prueba.
HDave
1
@HDave, creo que su solución con @PostConstructy @AfterClasssimplemente se comporta igual que @Beforey @After. De hecho, sus métodos se llamarán para cada método de prueba y no una vez para toda la clase (como indica Esko Luontola en su respuesta, se crea una instancia de clase para cada método de prueba). No puedo ver la utilidad de su solución, así que (a menos que me pierda algo)
magnum87
1
Ha estado funcionando correctamente durante 5 años, así que creo que mi solución funciona.
HDave el
13

La documentación de JUnit parece escasa, pero supongo: tal vez JUnit cree una nueva instancia de su clase de prueba antes de ejecutar cada caso de prueba, por lo que la única forma de que su estado de "fijación" persista en las ejecuciones es que sea estático, lo que puede ser ejecutado asegurándose de que su fixtureSetup (método @BeforeClass) sea estático.

Blair Conrad
fuente
2
No solo quizás, sino que JUnit definitivamente crea una nueva instancia de un caso de prueba. Entonces esta es la única razón.
guerda
Esta es la única razón por la que tienen, pero de hecho el corredor Junit podría hacer el trabajo de ejecutar los métodos BeforeTests y AfterTests como lo hace testng.
HDave el
¿TestNG crea una instancia de la clase de prueba y la comparte con todas las pruebas de la clase? Eso lo hace más vulnerable a los efectos secundarios entre pruebas.
Esko Luontola
3

Aunque esto no responderá a la pregunta original. Responderá al seguimiento obvio. Cómo crear una regla que funcione antes y después de una clase y antes y después de una prueba.

Para lograrlo puedes usar este patrón:

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

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

On before (Class) la JPAConnection crea la conexión una vez y después de (Class) la cierra.

getEntityMangerdevuelve una clase interna de JPAConnectionque implementa el EntityManager de jpa y puede acceder a la conexión dentro deljpaConnection . En antes (prueba) comienza una transacción después (prueba) la revierte nuevamente.

Esto no es seguro para subprocesos, pero se puede hacer que lo sea.

Código seleccionado 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
fuente
2

Parece que JUnit crea una nueva instancia de la clase de prueba para cada método de prueba. Prueba 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++);
    }
}

La salida es 0 0 0

Esto significa que si el método @BeforeClass no es estático, tendrá que ejecutarse antes de cada método de prueba y no habrá forma de diferenciar entre la semántica de @Before y @BeforeClass.

usuario aleatorio
fuente
No solo parece así, es así. La pregunta se ha hecho durante muchos años, aquí está la respuesta: martinfowler.com/bliki/JunitNewInstance.html
Paul
1

hay dos tipos de anotaciones:

  • @BeforeClass (@AfterClass) llamado una vez por clase de prueba
  • @Before (y @After) llamado antes de cada prueba

por lo que @BeforeClass debe declararse estático porque se llama una vez. También debe considerar que ser estático es la única forma de garantizar la propagación adecuada del "estado" entre las pruebas (el modelo JUnit impone una instancia de prueba por @Test) y, dado que en Java solo los métodos estáticos pueden acceder a datos estáticos ... @BeforeClass y @ AfterClass solo se puede aplicar a métodos estáticos.

Esta prueba de ejemplo debería aclarar el 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");
    }
}

salida:

------------- Salida estándar ---------------
antes de clase
antes de
prueba 1
después
antes de
prueba 2
después
después de clases
------------- ---------------- ---------------
dfa
fuente
19
Encuentro tu respuesta irrelevante. Conozco la semántica de BeforeClass y Before. Esto no explica por qué tiene que ser estático ...
ripper234
1
"Esto obliga a todos mis init a estar en miembros estáticos, sin ninguna buena razón por lo que veo". Mi respuesta debería mostrarle que su init también puede ser no estático usando @Before, en lugar de @BeforeClass
dfa
2
Me gustaría hacer algo de init solo una vez, al comienzo de la clase, pero en variables no estáticas.
ripper234
no puedes con JUnit, lo siento. Debes usar una variable estática, de ninguna manera.
dfa
1
Si la inicialización es costosa, puede mantener una variable de estado para registrar si ha realizado la inicialización y (verifíquela y, opcionalmente) realice la inicialización en un método @Before ...
Blair Conrad
0

Según JUnit 5, parece que la filosofía de crear estrictamente una nueva instancia por método de prueba se ha relajado un poco. Han agregado una anotación que creará una instancia de una clase de prueba solo una vez. Por lo tanto, esta anotación también permite que los métodos anotados con @ BeforeAll / @ AfterAll (los reemplazos de @ BeforeClass / @ AfterClass) sean no estáticos. Entonces, una clase de prueba 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);
    }
}

imprimiría:

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

Por lo tanto, puede crear instancias de objetos una vez por clase de prueba. Por supuesto, esto hace que sea su responsabilidad evitar la mutación de objetos que se instancian de esta manera.

EJJ
fuente
-11

Para resolver este problema, simplemente cambie el método

public void setUpBeforeClass 

a

public static void setUpBeforeClass()

y todo lo que se define en este método para static.

sri
fuente
2
Esto no responde la pregunta en absoluto.
rgargente