He estado escribiendo algunas pruebas unitarias para algún código nuevo en el trabajo, y lo envié para una revisión del código. Uno de mis compañeros de trabajo hizo un comentario sobre por qué estaba poniendo variables que se usan en varias de esas pruebas fuera del alcance de la prueba.
El código que publiqué fue esencialmente
import org.junit.Test;
public class FooUnitTests {
@Test
public void testConstructorWithValidName() {
new Foo(VALID_NAME);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithNullName() {
new Foo(null);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithZeroLengthName() {
new Foo("");
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
final String name = " " + VALID_NAME + " ";
final Foo foo = new Foo(name);
assertThat(foo.getName(), is(equalTo(VALID_NAME)));
}
private static final String VALID_NAME = "name";
}
Sus cambios propuestos fueron esencialmente
import org.junit.Test;
public class FooUnitTests {
@Test
public void testConstructorWithValidName() {
final String name = "name";
final Foo foo = new Foo(name);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithNullName() {
final String name = null;
final Foo foo = new Foo(name);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithZeroLengthName() {
final String name = "";
final Foo foo = new Foo(name);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
final String name = " name ";
final Foo foo = new Foo(name);
final String actual = foo.getName();
final String expected = "name";
assertThat(actual, is(equalTo(expected)));
}
}
Donde todo lo que se requiere dentro del alcance de la prueba, se define dentro del alcance de la prueba.
Algunas de las ventajas que argumentó fueron
- Cada prueba es independiente.
- Cada prueba se puede ejecutar de forma aislada o agregada, con el mismo resultado.
- El revisor no tiene que desplazarse a donde se declaren estos parámetros para buscar cuál es el valor.
Algunas de las desventajas de su método que argumentaba eran
- Aumenta la duplicación de código
- Puede agregar ruido a la mente de los revisores si hay varias pruebas similares con diferentes valores definidos (es decir, prueba con
doFoo(bar)
resultados en un valor, mientras que la misma llamada tiene un resultado diferente porquebar
se define de manera diferente en ese método).
Además de la convención, ¿hay otras ventajas / desventajas de usar cualquiera de los métodos sobre el otro?
java
unit-testing
tdd
Zymus
fuente
fuente
Respuestas:
Deberías seguir haciendo lo que estás haciendo.
Repetirse es una idea tan mala en el código de prueba como en el código comercial, y por la misma razón. Su colega ha sido engañado por la idea de que cada prueba debe ser autónoma . Eso es bastante cierto, pero "autocontenido" no significa que debe contener todo lo que necesita dentro de su cuerpo de método. Solo significa que debería dar el mismo resultado si se ejecuta en isolaton o como parte de una suite, e independientemente del orden de las pruebas en la suite. En otras palabras, el código de prueba debe tener la misma semántica independientemente de qué otro código se haya ejecutado antes ; no tiene que tener todo el código requerido incluido textualmente .
La reutilización de constantes, el código de configuración y similares aumenta la calidad del código de prueba y no compromete su autocontención, por lo que es bueno que continúe haciéndolo.
fuente
Depende. En general, tener constantes repetidas declaradas en un solo lugar es algo bueno.
Sin embargo, en su ejemplo, no tiene sentido definir un miembro
VALID_NAME
de la manera mostrada. Suponiendo que en la segunda variante un mantenedor cambia el textoname
en la primera prueba, la segunda prueba probablemente seguirá siendo válida y viceversa. Por ejemplo, supongamos que desea probar adicionalmente letras mayúsculas y minúsculas, pero también mantener un caso de prueba solo con letras minúsculas, con la menor cantidad posible de casos de prueba. En lugar de agregar una nueva prueba, puede cambiar la primera prueba ay aún conservan las pruebas restantes.
De hecho, tener muchas pruebas en función de dicha variable y cambiar el valor de
VALID_NAME
más tarde puede interrumpir involuntariamente algunas de sus pruebas en un momento posterior. Y en tal situación, de hecho puede mejorar la autocontención de sus pruebas al no introducir una constante artificial con diferentes pruebas dependiendo de ello.Sin embargo, lo que escribió @KilianFoth acerca de no repetirse también es correcto: siga el principio DRY en el código de prueba de la misma manera que lo hace en el código comercial. Por ejemplo, introduzca constantes o variables miembro cuando sea esencial para su código de prueba que su valor sea el mismo en todos los lugares.
Su ejemplo muestra cómo las pruebas tienden a repetir el código de inicialización, ese es un lugar típico para comenzar con la refactorización. Por lo tanto, puede considerar refactorizar la repetición de
en una función separada
ya que le permitirá cambiar la firma del
Foo
constructor en un momento posterior con mayor facilidad (porque hay menos lugares dondenew Foo
realmente se llama).fuente
Yo en realidad ... no iría por ambos , pero elegiré el tuyo sobre tu compañero de trabajo por tu ejemplo simplificado.
Lo primero es lo primero, tiendo a favorecer valores en línea en lugar de hacer tareas únicas. Esto mejora la legibilidad del código para mí, ya que no tengo que dividir mi línea de pensamiento en múltiples declaraciones de asignación, luego leer la (s) línea (s) de código donde se usan. Por lo tanto, preferiré su enfoque sobre su compañero de trabajo, con el ligero inconveniente que
testConstructorWithLeadingAndTrailingWhitespaceInName()
probablemente pueda ser solo:Eso me lleva a los nombres. Por lo general, si su prueba afirma
Exception
que se lanzará, su nombre también debe describir ese comportamiento para mayor claridad. Usando el ejemplo anterior, ¿qué sucede si tengo espacios en blanco adicionales en el nombre cuando construyo miFoo
objeto? ¿Y cómo se relaciona eso con lanzar unIllegalArgumentException
?Con base en las pruebas
null
y""
, supongo queException
esta vez se lanza lo mismo para llamargetName()
. Si ese es el caso, quizás sea mejor establecer el nombre del método de prueba comocallingGetNameWithWhitespacePaddingThrows()
(IllegalArgumentException
que creo que será parte de la salida de JUnit, por lo tanto opcional). Si tener espacios en blanco en el nombre durante la creación de instancias también arrojará lo mismoException
, entonces también omitiré la afirmación por completo, para dejar en claro que está en el comportamiento del constructor.Sin embargo, creo que hay una mejor manera de hacer las cosas aquí cuando obtienes más permutaciones para entradas válidas e inválidas, es decir, pruebas parametrizadas . Lo que está probando es exactamente eso: está creando un montón de
Foo
objetos con diferentes argumentos de constructor, y luego afirmando que falla en la instanciación o en la llamadagetName()
. Por lo tanto, ¿por qué no dejar que JUnit realice la iteración y el andamiaje por usted? Puede, en términos simples, decirle a JUnit, "estos son los valores con los que quiero crear unFoo
objeto", y luego hacer que su método de prueba afirmeIllegalArgumentException
en los lugares correctos Desafortunadamente, dado que la forma de pruebas parametrizadas de JUnit no funciona bien con las pruebas para casos exitosos y excepcionales en la misma prueba (que yo sepa), es probable que tengas que saltar un poco de aro con la siguiente estructura:Es cierto que esto parece torpe para lo que es una prueba de cuatro valores, que de otro modo sería simple, y la implementación algo restrictiva de JUnit no lo ayuda mucho. En este sentido, considero que el
@DataProvider
enfoque de TestNG es más útil, y definitivamente vale la pena verlo más de cerca si está considerando pruebas parametrizadas.Así es como uno podría usar TestNG para sus pruebas unitarias (usando Java 8
Stream
para simplificar laIterator
construcción). Algunos de los beneficios son, pero no se limitan a:@DataProvider
correos electrónicos que proporcionen diferentes tipos de entradas.fuente