java - unitaria - qué anotación indica que un método será una prueba
Unidad de Java sólida automatización de prueba?(JUnit/Hamcrest/…) (4)
Intención
Estoy buscando lo siguiente:
- Una metodología de prueba de unidad sólida
- ¿Qué me estoy perdiendo de mi enfoque?
- ¿Qué estoy haciendo mal ?
- ¿Qué estoy haciendo que es innecesario?
- Una forma de hacer todo lo posible automáticamente
Entorno actual
- Eclipse como IDE
- JUnit como framework de testing, integrado en Eclipse.
- Hamcrest como una biblioteca de "emparejadores", para una mejor legibilidad de afirmación
- Google Guava para la validación de la condición previa
Enfoque actual
Estructura
- Una clase de prueba por clase para probar
- Método de prueba agrupado en clases anidadas estáticas
- Nombre del método de prueba para especificar el comportamiento probado + resultado esperado
- Excepciones esperadas especificadas por Annotation Java, no en el nombre del método
Metodología
- Cuidado con
null
valoresnull
- Cuidado con la List<E> vacía List<E>
- Cuidado con la String vacía
- Cuidado con las matrices vacías
- Cuidado con las invariantes de estado de objeto alteradas por código (post-condiciones)
- Los métodos aceptan tipos de parámetros documentados
- Verificaciones de límites (por ejemplo, Integer.MAX_VALUE , etc ...)
- Documentación de la inmutabilidad a través de tipos específicos (por ejemplo, Google Guava ImmutableList<E> )
- ... hay una lista para esto? Ejemplos de listas de pruebas agradables para tener:
- Cosas para verificar en proyectos de base de datos (por ejemplo, CRUD, conectividad, registro, ...)
- Cosas para comprobar en código multiproceso
- Cosas para verificar los EJBs
- ...?
Código de muestra
Este es un ejemplo artificial para mostrar algunas técnicas.
MyPath.java
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Arrays;
import com.google.common.collect.ImmutableList;
public class MyPath {
public static final MyPath ROOT = MyPath.ofComponents("ROOT");
public static final String SEPARATOR = "/";
public static MyPath ofComponents(String... components) {
checkNotNull(components);
checkArgument(components.length > 0);
checkArgument(!Arrays.asList(components).contains(""));
return new MyPath(components);
}
private final ImmutableList<String> components;
private MyPath(String[] components) {
this.components = ImmutableList.copyOf(components);
}
public ImmutableList<String> getComponents() {
return components;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (String pathComponent : components) {
stringBuilder.append("/" + pathComponent);
}
return stringBuilder.toString();
}
}
MyPathTests.java
import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import com.google.common.base.Joiner;
@RunWith(Enclosed.class)
public class MyPathTests {
public static class GetComponents {
@Test
public void componentsCorrespondToFactoryArguments() {
String[] components = { "Test1", "Test2", "Test3" };
MyPath myPath = MyPath.ofComponents(components);
assertThat(myPath.getComponents(), contains(components));
}
}
public static class OfComponents {
@Test
public void acceptsArrayOfComponents() {
MyPath.ofComponents("Test1", "Test2", "Test3");
}
@Test
public void acceptsSingleComponent() {
MyPath.ofComponents("Test1");
}
@Test(expected = IllegalArgumentException.class)
public void emptyStringVarArgsThrows() {
MyPath.ofComponents(new String[] { });
}
@Test(expected = NullPointerException.class)
public void nullStringVarArgsThrows() {
MyPath.ofComponents((String[]) null);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsInterspersedEmptyComponents() {
MyPath.ofComponents("Test1", "", "Test2");
}
@Test(expected = IllegalArgumentException.class)
public void rejectsSingleEmptyComponent() {
MyPath.ofComponents("");
}
@Test
public void returnsNotNullValue() {
assertThat(MyPath.ofComponents("Test"), is(notNullValue()));
}
}
public static class Root {
@Test
public void hasComponents() {
assertThat(MyPath.ROOT.getComponents(), is(not(empty())));
}
@Test
public void hasExactlyOneComponent() {
assertThat(MyPath.ROOT.getComponents(), hasSize(1));
}
@Test
public void hasExactlyOneInboxComponent() {
assertThat(MyPath.ROOT.getComponents(), contains("ROOT"));
}
@Test
public void isNotNull() {
assertThat(MyPath.ROOT, is(notNullValue()));
}
@Test
public void toStringIsSlashSeparatedAbsolutePathToInbox() {
assertThat(MyPath.ROOT.toString(), is(equalTo("/ROOT")));
}
}
public static class ToString {
@Test
public void toStringIsSlashSeparatedPathOfComponents() {
String[] components = { "Test1", "Test2", "Test3" };
String expectedPath =
MyPath.SEPARATOR + Joiner.on(MyPath.SEPARATOR).join(components);
assertThat(MyPath.ofComponents(components).toString(),
is(equalTo(expectedPath)));
}
}
@Test
public void testPathCreationFromComponents() {
String[] pathComponentArguments = new String[] { "One", "Two", "Three" };
MyPath myPath = MyPath.ofComponents(pathComponentArguments);
assertThat(myPath.getComponents(), contains(pathComponentArguments));
}
}
Pregunta, expresada expresamente
¿Hay una lista de técnicas para usar para construir una prueba de unidad? ¿Algo mucho más avanzado que mi lista simplificada más arriba (p. Ej., Verificar nulos, verificar límites, verificar excepciones esperadas, etc.) tal vez disponible en un libro para comprar o una URL para visitar?
Una vez que tengo un método que toma un cierto tipo de parámetros, ¿puedo obtener algún complemento de Eclipse para generar un código auxiliar para mis pruebas para mí? ¿Tal vez usar una Annotation Java para especificar los metadatos sobre el método y hacer que la herramienta materialice los controles asociados para mí? (por ejemplo, @MustBeLowerCase, @ShouldBeOfSize (n = 3), ...)
Me parece tedioso y como un robot tener que recordar todos estos "trucos de control de calidad" y / o aplicarlos, me parece propenso a errores copiar y pegar y no se autodocumenta cuando codifico las cosas como lo hago encima. Es cierto que las libraries Hamcrest van en la dirección general de la especialización de tipos de pruebas (p. Ej., En objetos String que usan RegEx, en objetos File , etc.) pero obviamente no generan automáticamente talones de prueba y no reflejan el código y sus propiedades, y preparan Un arnés para mí.
Ayúdame a mejorar esto, por favor.
PD
No me diga que solo estoy presentando un código que es una envoltura tonta en torno al concepto de crear una Ruta a partir de una lista de pasos de ruta proporcionados en un método de fábrica estática, este es un ejemplo totalmente inventivo pero muestra "algunos "casos de validación de argumentos ... Si incluyera un ejemplo mucho más largo, ¿quién realmente leería esta publicación?
Considere usar ExpectedException lugar de
@Test(expected...
Esto se debe a que, por ejemplo, espera unaNullPointerException
y su prueba arroja esta excepción en su configuración (antes de llamar al método bajo prueba) su prueba pasará. ConExpectedException
, coloque la esperar inmediatamente antes de la llamada al método bajo prueba, por lo que no hay ninguna posibilidad de esto. Además,ExpectedException
permite probar el mensaje de excepción, que es útil si tiene dosIllegalArgumentExceptions
diferentes y debe buscar el correcto .Considere aislar su método bajo prueba de la configuración y verifique, esto facilitará la revisión y el mantenimiento de la prueba. Esto es especialmente cierto cuando los métodos en la clase bajo prueba se invocan como parte de la configuración que puede confundir cuál es el método bajo prueba. Yo uso el siguiente formato:
public void test() { //setup ... // test (usually only one line of code in this block) ... //verify ... }
Libros para ver: Clean Code , JUnit In Action , Test Driven Development By Example
Código limpio tiene una excelente sección de pruebas
La mayoría de los ejemplos que he visto (incluido lo que Eclipse genera automáticamente) tienen el método bajo prueba en el título de la prueba. Esto facilita la revisión y el mantenimiento. Por ejemplo:
testOfComponents_nullCase
. Su ejemplo es el primero que he visto que utiliza el métodoEnclosed
to group por el método bajo prueba, lo cual es realmente bueno. Sin embargo, agrega cierta sobrecarga ya que@Before
y@Before
no se comparten entre clases de prueba adjuntas.No he empezado a usarlo, pero Guava tiene una biblioteca de prueba: guava-testlib . No he tenido la oportunidad de jugar con él, pero parece tener algunas cosas geniales. Por ejemplo: NullPointerTest is quote:
- Una utilidad de prueba que verifica que sus métodos arrojan {@link * NullPointerException} o {@link UnsupportedOperationException} siempre que cualquiera * de sus parámetros sea nulo. Para usarlo, primero debe proporcionar valores predeterminados * válidos para los tipos de parámetros utilizados por la clase.
Revisión: Me doy cuenta de que la prueba anterior fue solo un ejemplo, pero como una revisión constructiva podría ser útil, aquí tienes.
Al probar
getComponents
, pruebe el caso de la lista vacía también. También, useIsIterableContainingInOrder
.En las pruebas de los
ofComponents
, parece que tendría sentido llamar agetComponents
otoString
para validar que manejó adecuadamente los diversos casos sin errores. Debe haber una prueba donde no se pasa ningún argumento a losofComponents
. Veo que esto se hace conofComponents( new String[]{})
pero ¿por qué no hacer simplementeofComponents()
? Necesita una prueba dondenull
sea uno de los valores pasados:ofComponents("blah", null, "blah2")
ya que esto arrojará un NPE.Al probar
ROOT
, como se ha señalado anteriormente, sugiero llamarROOT.getComponents
una vez y hacer las tres verificaciones en él. Además,ItIterableContainingInOrder
hace los tres no vacíos, tamaño y contenido. El examenis
en las pruebas extrañas (aunque es lingüístico) y creo que no vale la pena (IMHO).En las pruebas
toString
, creo que es muy útil aislar el método bajo prueba. Habría escrito entoStringIsSlashSeparatedPathOfComponents
siguiente manera. Note que no uso la constante de la clase bajo prueba. Esto se debe a que, en mi humilde opinión, CUALQUIER cambio funcional en la clase bajo prueba debería hacer que la prueba falle.@Test public void toStringIsSlashSeparatedPathOfComponents() { //setup String[] components = { "Test1", "Test2", "Test3" }; String expectedPath = "/" + Joiner.on("/").join(components); MyPath path = MyPath.ofComponents(components) // test String value = path.toStrign(); // verify assertThat(value, equalTo(expectedPath)); }
Enclosed
no ejecutará ninguna prueba unitaria que no esté en una clase interna. PortestPathCreationFromComponents
tanto,testPathCreationFromComponents
no se ejecutaría.
Finalmente, use el desarrollo dirigido por prueba. Esto asegurará que sus pruebas se aprueben por la razón correcta y fallarán como se esperaba.
Bueno, voy a publicar 2 respuestas diferentes.
Como James Coplien declaró que la prueba de unidad no vale nada. No estoy de acuerdo con él en este tema, pero puede que le resulte útil considerar la unidad de prueba menos en lugar de buscar una solución automática.
Considera utilizar teorías con puntos de datos. Creo que esto minimizará significativamente su problema. Además, usar el simulacro puede ayudarte.
Ok, aquí está mi opinión sobre sus preguntas:
¿Hay una lista de técnicas para usar para construir una prueba de unidad?
Respuesta corta, no. Su problema es que para generar una prueba para un método, debe analizar qué hace y poner una prueba para cada valor posible en cada lugar. Hay / fueron generadores de prueba, pero IIRC, no generaron código mantenible (ver Recursos para el desarrollo guiado por prueba ).
Ya tienes una lista bastante buena de cosas para verificar, a las que añadiría:
- Asegúrese de que todos los caminos a través de sus métodos están cubiertos.
- Asegúrese de que todas las funciones importantes estén cubiertas por más de una prueba; uso mucho Parametrizado para esto.
Una cosa que me parece realmente útil es preguntar qué debería hacer este método, en lugar de qué hace este método. De esta manera, escribes las pruebas con una mente más abierta.
Otra cosa que me parece útil es reducir la placa de caligrafía asociada con las pruebas, para poder leerlas más fácilmente. Cuanto más fácil sea agregar pruebas, mejor. Me parece muy bien parametrizado para esto. Para mí, la legibilidad de las pruebas es clave.
Entonces, tomando el ejemplo anterior, si eliminamos el requisito ''solo probamos una cosa en un método'' obtenemos
public static class Root {
@Test
public void testROOT() {
assertThat("hasComponents", MyPath.ROOT.getComponents(), is(not(empty())));
assertThat("hasExactlyOneComponent", MyPath.ROOT.getComponents(), hasSize(1));
assertThat("hasExactlyOneInboxComponent", MyPath.ROOT.getComponents(), contains("ROOT"));
assertThat("isNotNull", MyPath.ROOT, is(notNullValue()));
assertThat("toStringIsSlashSeparatedAbsolutePathToInbox", MyPath.ROOT.toString(), is(equalTo("/ROOT")));
}
}
He hecho dos cosas, he agregado la descripción en la afirmación y he fusionado todas las pruebas en una. Ahora, podemos leer la prueba y ver que tenemos pruebas duplicadas. Probablemente no is(notNullValue())
que probar is(not(empty())
is(notNullValue())
, etc. Esto viola la regla de una afirmación por método, pero creo que está justificado porque ha eliminado un montón de repetitivo sin cortar Abajo en cobertura.
¿Puedo realizar comprobaciones automáticamente?
Sí. Pero no usaría anotaciones para hacerlo. Digamos que tenemos un método como:
public boolean validate(Foobar foobar) {
return !foobar.getBar().length > 40;
}
Así que tengo un método de prueba que dice algo como:
private Foobar getFoobar(int length) {
Foobar foobar = new Foobar();
foobar.setBar(StringUtils.rightPad("", length, "x")); // make string of length characters
return foobar;
}
@Test
public void testFoobar() {
assertEquals(true, getFoobar(39));
assertEquals(true, getFoobar(40));
assertEquals(false, getFoobar(41));
}
El método anterior es bastante fácil de calcular, dependiendo de la longitud, en una prueba parametrizada, por supuesto. Moraleja de la historia, puede factorizar sus pruebas como puede hacerlo con un código que no sea de prueba.
Entonces, para responder a su pregunta, en mi experiencia, he llegado a la conclusión de que puede hacer mucho para ayudar con todas las combinaciones al reducir el número de participantes en sus pruebas, mediante el uso de una combinación acertada de Parametrización y factorización de su pruebas Como ejemplo final, así es como implementaría su prueba con Parametrizado:
@RunWith (Parameterized.class) clase estática pública OfComponents {@Parameters public static Recopilación de datos () {return Arrays.asList (nuevo objeto [] [] {{new String [] {"Test1", "Test2", "Test3" }, null}, {new String [] {"Test1"}, null}, {null, NullPointerException.class}, {new String [] {"Test1", "", "Test2"}, IllegalArgumentException},}) ; }
private String[] components;
@Rule
public TestRule expectedExceptionRule = ExpectedException.none();
public OfComponents(String[] components, Exception expectedException) {
this.components = components;
if (expectedException != null) {
expectedExceptionRule.expect(expectedException);
}
}
@Test
public void test() {
MyPath.ofComponents(components);
}
Tenga en cuenta que lo anterior no se ha probado y probablemente no se compila. De lo anterior, puede analizar los datos como entrada y agregar (o al menos pensar en agregar) todas las combinaciones de todo. Por ejemplo, no tiene una prueba para {"Prueba1", nula, "Prueba2"} ...
Veo que te esfuerzas mucho para poner a prueba tus clases. ¡Bueno! :)
Mis comentarios / preguntas serían:
- ¿Qué hay de burlarse? No mencionas ninguna herramienta para esto.
- Me parece que te preocupas mucho por los detalles esenciales (¡no digo que no sean importantes!), mientras que descuidas el propósito comercial de la clase examinada. Supongo que viene del hecho de que codificas primero el código (¿verdad?). Lo que sugeriría es un enfoque más TDD / BDD y un enfoque en las responsabilidades comerciales de la clase probada.
- no está seguro de lo que esto le proporciona: "Prueba de método agrupada en clases anidadas estáticas"?
- con respecto a la autogeneración de comprobantes de prueba, etc. En pocas palabras: no. Terminarás probando la implementación en lugar del comportamiento.