Práctica recomendada: ¿Inicializar los campos de la clase JUnit en setUp () o en la declaración?

120

¿Debo inicializar los campos de clase en una declaración 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
    }
}

¿O en setUp () así?

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
    }
}

Tiendo a usar el primer formulario porque es más conciso y me permite usar campos finales. Si no necesito utilizar el método setUp () para la configuración, ¿debería seguir utilizándolo y por qué?

Aclaración: JUnit creará una instancia de la clase de prueba una vez por método de prueba. Eso significa listque se creará una vez por prueba, independientemente de dónde lo declare. También significa que no hay dependencias temporales entre las pruebas. Por tanto, parece que no hay ventajas en utilizar setUp (). Sin embargo, las preguntas frecuentes de JUnit tienen muchos ejemplos que inicializan una colección vacía en setUp (), así que supongo que debe haber una razón.

Craig P. Motlin
fuente
2
Tenga en cuenta que la respuesta difiere en JUnit 4 (inicializar en la declaración) y JUnit 3 (usar setUp); esta es la raíz de la confusión.
Nils von Barth
Véase también stackoverflow.com/questions/6094081/…
Grigory Kislin

Respuestas:

99

Si se está preguntando específicamente sobre los ejemplos en las preguntas frecuentes de JUnit, como la plantilla de prueba básica , creo que la mejor práctica que se muestra allí es que la clase bajo prueba debe instanciarse en su método de configuración (o en un método de prueba) .

Cuando los ejemplos de JUnit crean una ArrayList en el método setUp, todos pasan a probar el comportamiento de esa ArrayList, con casos como testIndexOutOfBoundException, testEmptyCollection y similares. La perspectiva que existe es la de alguien que escribe una clase y se asegura de que funcione correctamente.

Probablemente debería hacer lo mismo cuando pruebe sus propias clases: cree su objeto en setUp o en un método de prueba, para que pueda obtener un resultado razonable si lo rompe más tarde.

Por otro lado, si usa una clase de colección Java (u otra clase de biblioteca, para el caso) en su código de prueba, probablemente no sea porque quiera probarlo, es solo parte del dispositivo de prueba. En este caso, puede asumir con seguridad que funciona según lo previsto, por lo que inicializarlo en la declaración no será un problema.

Por lo que vale, trabajo en una base de código relativamente grande, desarrollada por TDD y de varios años de antigüedad. Habitualmente inicializamos cosas en sus declaraciones en código de prueba, y en el año y medio que he estado en este proyecto, nunca ha causado un problema. Entonces, hay al menos alguna evidencia anecdótica de que es algo razonable.

Columna de musgo
fuente
45

Empecé a investigar y encontré una ventaja potencial de usar setUp(). Si se produce alguna excepción durante la ejecución de setUp(), JUnit imprimirá un seguimiento de pila muy útil. Por otro lado, si se lanza una excepción durante la construcción del objeto, el mensaje de error simplemente dice que JUnit no pudo instanciar el caso de prueba y no ve el número de línea donde ocurrió la falla, probablemente porque JUnit usa la reflexión para instanciar la prueba. clases.

Nada de esto se aplica al ejemplo de crear una colección vacía, ya que eso nunca arrojará, pero es una ventaja del setUp()método.

Craig P. Motlin
fuente
18

Además de la respuesta de Alex B.

Incluso es necesario utilizar el método setUp para crear instancias de recursos en un estado determinado. Hacer esto en el constructor no es solo una cuestión de tiempos, sino que debido a la forma en que JUnit ejecuta las pruebas, cada estado de prueba se borraría después de ejecutar una.

JUnit primero crea instancias de testClass para cada método de prueba y comienza a ejecutar las pruebas después de que se crea cada instancia. Antes de ejecutar el método de prueba, se ejecuta su método de configuración, en el que se puede preparar algún estado.

Si el estado de la base de datos se creara en el constructor, todas las instancias crearían una instancia del estado de la base de datos una después de la otra, antes de ejecutar cada prueba. A partir de la segunda prueba, las pruebas se ejecutarían con un estado sucio.

Ciclo de vida de JUnits:

  1. Cree una instancia de clase de prueba diferente para cada método de prueba
  2. Repita para cada instancia de clase de prueba: llame a setup + llame al método de prueba

Con algunos registros en una prueba con dos métodos de prueba, obtienes: (el número es el código hash)

  • Creando nueva instancia: 5718203
  • Creando nueva instancia: 5947506
  • Configuración: 5718203
  • TestOne: 5718203
  • Configuración: 5947506
  • Prueba dos: 5947506
Jurgen Hannaert
fuente
3
Correcto, pero fuera de tema. La base de datos es esencialmente un estado global. Este no es un problema al que me enfrento. Me preocupa simplemente la velocidad de ejecución de las pruebas debidamente independientes.
Craig P. Motlin
Este orden de inicialización solo es cierto en JUnit 3, donde es una precaución importante. En JUnit 4, las instancias de prueba se crean de forma perezosa, por lo que la inicialización en la declaración o en un método de configuración ocurre en el momento de la prueba. También para una configuración única, se puede usar @BeforeClassen JUnit 4.
Nils von Barth
11

En JUnit 4:

  • Para la Clase bajo prueba , inicialice un @Beforemétodo para detectar fallas.
  • Para otras clases , inicialice en la declaración ...
    • ... por brevedad y para marcar los campos final, exactamente como se indica en la pregunta,
    • ... a menos que sea una inicialización compleja que pueda fallar, en cuyo caso utilícela @Beforepara detectar fallas.
  • Para el estado global (especialmente la inicialización lenta , como una base de datos), use @BeforeClass, pero tenga cuidado con las dependencias entre las pruebas.
  • Por supuesto, la inicialización de un objeto utilizado en una única prueba debe realizarse en el propio método de prueba.

La inicialización en un @Beforemétodo o método de prueba le permite obtener mejores informes de errores sobre fallas. Esto es especialmente útil para crear instancias de la clase bajo prueba (que puede romper), pero también es útil para llamar a sistemas externos, como el acceso al sistema de archivos ("archivo no encontrado") o la conexión a una base de datos ("conexión rechazada").

Es aceptable tener un estándar simple y usarlo siempre @Before(errores claros pero detallados) o inicializar siempre en una declaración (conciso pero da errores confusos), ya que las reglas de codificación complejas son difíciles de seguir, y esto no es gran cosa.

La inicialización setUpes una reliquia de JUnit 3, donde todas las instancias de prueba se inicializaron con entusiasmo, lo que causa problemas (velocidad, memoria, agotamiento de recursos) si realiza una inicialización costosa. Por lo tanto, la mejor práctica fue realizar una inicialización costosa setUp, que solo se ejecutó cuando se ejecutó la prueba. Esto ya no se aplica, por lo que es mucho menos necesario utilizarlo setUp.

Esto resume varias otras respuestas que entierran el lede, en particular por Craig P. Motlin (pregunta en sí y auto-respuesta), Moss Collum (clase bajo prueba) y dsaff.

Nils von Barth
fuente
7

En JUnit 3, sus inicializadores de campo se ejecutarán una vez por método de prueba antes de ejecutar cualquier prueba . Siempre que los valores de campo sean pequeños en la memoria, requieran poco tiempo de configuración y no afecten al estado global, el uso de inicializadores de campo está técnicamente bien. Sin embargo, si no se cumplen, puede terminar consumiendo mucha memoria o tiempo configurando sus campos antes de que se ejecute la primera prueba, y posiblemente incluso quedando sin memoria. Por esta razón, muchos desarrolladores siempre establecen valores de campo en el método setUp (), donde siempre es seguro, incluso cuando no es estrictamente necesario.

Tenga en cuenta que en JUnit 4, la inicialización del objeto de prueba ocurre justo antes de la ejecución de la prueba, por lo que el uso de inicializadores de campo es más seguro y el estilo recomendado.

dsaff
fuente
Interesante. Entonces, ¿el comportamiento que describiste al principio solo se aplica a JUnit 3?
Craig P. Motlin
6

En su caso (creando una lista) no hay diferencia en la práctica. Pero generalmente es mejor usar setUp (), porque eso ayudará a Junit a reportar las Excepciones correctamente. Si ocurre una excepción en el constructor / inicializador de una prueba, eso es un error de prueba . Sin embargo, si se produce una excepción durante la configuración, es natural pensar en ello como un problema al configurar la prueba, y junit lo informa de manera adecuada.

amit
fuente
1
bien dicho. Solo acostúmbrese a crear siempre una instancia en setUp () y tendrá una pregunta menos de la que preocuparse, por ejemplo, ¿dónde debería instanciar mi fooBar, dónde mi colección? Es una especie de estándar de codificación que solo debe cumplir. No le beneficia con listas, sino con otras instancias.
Olaf Kock
@Olaf Gracias por la información sobre el estándar de codificación, no había pensado en eso. Sin embargo, tiendo a estar más de acuerdo con la idea de Moss Collum de un estándar de codificación.
Craig P. Motlin
5

Prefiero la legibilidad primero, que a menudo no usa el método de configuración. Hago una excepción cuando una operación de configuración básica lleva mucho tiempo y se repite dentro de cada prueba.
En ese punto, muevo esa funcionalidad a un método de configuración usando la @BeforeClassanotación (optimizar más tarde).

Ejemplo de optimización usando el @BeforeClassmétodo de configuración: uso dbunit para algunas pruebas funcionales de la base de datos. El método de configuración es responsable de poner la base de datos en un estado conocido (muy lento ... 30 segundos - 2 minutos dependiendo de la cantidad de datos). Carga estos datos en el método de configuración anotado @BeforeClassy luego ejecuto de 10 a 20 pruebas con el mismo conjunto de datos en lugar de volver a cargar / inicializar la base de datos dentro de cada prueba.

Usar Junit 3.8 (extender TestCase como se muestra en su ejemplo) requiere escribir un poco más de código que simplemente agregar una anotación, pero la "ejecución una vez antes de la configuración de la clase" aún es posible.

Alex B
fuente
1
+1 porque también prefiero la legibilidad. Sin embargo, no estoy convencido de que la segunda forma sea una optimización en absoluto.
Craig P. Motlin
@Motlin Agregué el ejemplo de dbunit para aclarar cómo se puede optimizar con la configuración.
Alex B
La base de datos es esencialmente un estado global. Así que mover la configuración de db a setUp () no es una optimización, es necesario que las pruebas se completen correctamente.
Craig P. Motlin
@Alex B: Como dijo Motlin, esto no es una optimización. Solo está cambiando en qué parte del código se realiza la inicialización, pero no cuántas veces ni qué tan rápido.
Eddie
Tenía la intención de implicar el uso de la anotación "@BeforeClass". Editando el ejemplo para aclarar.
Alex B
2

Dado que cada prueba se ejecuta de forma independiente, con una instancia nueva del objeto, no tiene mucho sentido que el objeto de prueba tenga un estado interno excepto el compartido entre setUp()una prueba individual y tearDown(). Esta es una de las razones (además de las razones que dieron otros) por la que es bueno utilizar el setUp()método.

Nota: ¡Es una mala idea que un objeto de prueba JUnit mantenga un estado estático! Si utiliza una variable estática en sus pruebas para cualquier otra cosa que no sea el seguimiento o el diagnóstico, está invalidando parte del propósito de JUnit, que es que las pruebas pueden (y pueden) ejecutarse en cualquier orden, cada prueba ejecutándose con un Estado fresco y limpio.

Las ventajas de usarlo setUp()es que no tiene que cortar y pegar código de inicialización en cada método de prueba y que no tiene código de configuración de prueba en el constructor. En tu caso, hay poca diferencia. La simple creación de una lista vacía se puede hacer de forma segura como la muestra o en el constructor, ya que es una inicialización trivial. Sin embargo, como usted y otros han señalado, cualquier cosa que pueda generar un error Exceptiondebe hacerse setUp()para que obtenga el volcado de pila de diagnóstico si falla.

En su caso, donde solo está creando una lista vacía, haría lo mismo que está sugiriendo: Asignar la nueva lista en el punto de declaración. Especialmente porque de esta manera tienes la opción de marcarlo finalsi esto tiene sentido para tu clase de prueba.

Eddie
fuente
1
+1 porque eres la primera persona que apoya la inicialización de la lista durante la construcción del objeto para marcarla como final. Sin embargo, el tema de las variables estáticas está fuera del tema de la pregunta.
Craig P. Motlin
@Motlin: cierto, lo de las variables estáticas está un poco fuera de tema. No estoy seguro de por qué agregué eso, pero me pareció apropiado en ese momento, una extensión de lo que estaba diciendo en el primer párrafo.
Eddie
Sin finalembargo, la ventaja de se menciona en la pregunta.
Nils von Barth
0
  • Los valores constantes (usos en accesorios o aserciones) deben inicializarse en sus declaraciones y final(como nunca cambian)

  • el objeto bajo prueba debe inicializarse en el método de configuración porque podemos configurar las cosas. Por supuesto, es posible que no establezcamos algo ahora, pero podríamos configurarlo más tarde. Crear instancias en el método init facilitaría los cambios.

  • las dependencias del objeto bajo prueba si se simulan, ni siquiera deberían ser instanciadas por usted mismo: hoy los marcos simulados pueden instanciarlo por reflexión.

Una prueba sin dependencia para simular podría verse así:

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()
         ...
    }
}

Una prueba con dependencias para aislar podría verse así:

@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
fuente