
Si hablamos de frameworks para correr test en Java, a casi todos (para no decir todos) se le viene a la cabeza JUnit, pero hace un par de años surgió TestNG, un framework bastante completo, éste fue pionero el uso de annotations para testing en Java. Hasta ese momento existía JUnit 3, que no contaba con esta funcionalidad aunque luego en su versión 4 incorporó el uso. En este post vamos a analizar algunas de las funcionalidades, usos, ventajas y desventajas de cada uno.
JUnit fue el framework de testing más adoptado por la comunidad Javera, cuyo desarrollo empezó en un vuelo que compartieron dos conocidos por el mundo de desarrollo, Kent Beck y Erich Gamma. Beck quería aprender Java, y Gamma quería aprender sobre el framework de testing para Smaltalk de Beck (sUnit). Luego de 3 horas de viaje ya tenían las bases de JUnit.
En la versión inicial de JUnit, para implementar tests uno tenía que extender una clase base, y luego iniciar el nombre del test justamente con la palabra test. Por lo que un test se vería de la siguiente forma:
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
public void testAddition() {
int z = 1 + 1;
assertEquals(2, z);
}
}
Junit se encarba de buscar todos los métodos que comiencen con test para ejecutarlos.
JUnit 4 en esencia es muy similar a su antecesor, aunque con un gran cambio en su arquitectura. A partir de JUnit 4 se deja de usar extensión para definir las clases de Test y se deja de lado la restricción de nombres de test (iniciar los nombres con las letras “test”). En esta nueva versión adoptan el uso de annotations, en el cual se anotan los métodos que son test y.
El mismo test implementado (arriba) con JUnit 4 quedaría de la siguiente forma:
public class AdditionTest {
@Test
public void theResultOfAddingOneAndOneShouldBeTwo() {
int z = 1 + 1;
assertEquals(2, z);
}
}
En este caso la clase AdditionTest no tiene que extender de BaseTest y el nombre del método puede empezar como más nos convenga, lo único que hace que sea un test es el hecho de que esté anotado con “@Test”.
Hace un par de años surgió TestNG como una alternativa a JUnit (en ese momento estaba la versión 3). Una de sus características más novedosas fue el uso de annotations (luego también adoptado por JUnit en su versión 4).
A la hora de escribir test de unidad sencillos, el código para usar cualquiera de ambos (JUnit / TestNG) es muy similar. En este primer ejemplo solo vamos a encontrar una sola diferencia, y difícil de ver:
public class AdditionTest {
@Test
public void theResultOfAddingOneAndShouldBeTwo() {
int z = 1 + 1;
assertEquals(z, 2);
}
}
Como podemos observar en el ejemplo, ambos utilizan el mismo nombre para la annotation y solo difieren en el orden de los parámetros en el assert.:
Una diferencia bastante notoria entre JUnit y TestNG es la forma de implementar tests suites. En JUnit se usan clases con annotations (como para los tests) para definir una suite. Cuando uno crea una suite tiene que declarar que clases de tests quiere correr:
@RunWith(Suite.class)
@SuiteClasses({ PersonPersisterTest.class })
public class PersonSuite {
}
@RunWith(Suite.class)
@SuiteClasses({ PersonSuite.class, CountryIsoCodeValidatorTest.class })
public class AllSuite {
}
En la primer suite declaramos que se va a usar el runner de Suite y además que clases de tests queremos que corra nuetra suite. En la segunda vemos que una suite puede estar incluida en otra.
En TestNG la forma de crear tests suites cambia por completo, ni siquiera usa clases, se crean por medio de archivos xml, el archivo xml que se ejecuta por defecto es el testng.xml y en caso de no existir, TestNG crea uno temporal por nosotros. En el mismo ejemplo dado en JUnit con TestNG tendríamos 2 archivos xml distintos:
personSuite.xml
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd"> <suite name="PersonSuite"> <test name="person-tests"> <classes> <class name="com.hexacta.hat.testingframeworks.testng.PersonPersisterTest" /> </classes> </test> </suite>
allSuite.xml
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd"> <suite name="AllSuite"> <suite-files> <suite-file path="PersonSuite.xml" /> </suite-files> <test name="country-iso-code"> <classes> <class name="com.hexacta.hat.testingframeworks.testng.CountryIsoCodeValidatorTest" /> </classes> </test> </suite>
Estos archivos xml de TestNG siguen la siguiente terminología:
Varias veces uno necesita testear un método en particular utilizando un sets de datos específicos. Supongamos que tenemos un validador de códigos de países ISO 3166-2. En este caso vamos a querer testear que ingresando cualquier código válido, la validación pase exitosamente. En JUnit 3, tendríamos que crear un test para cáda código, en cambio tanto JUnit 4 como TestNG nos proveen una herramienta para escribir un solo test que reciba un párametro y proveedor de datos para el método de test.
@RunWith(value = Parameterized.class)
public class CountryIsoCodeValidatorTest {
private String code;
public CountryIsoCodeValidatorTest(final String code) {
this.code = code;
}
@Parameters
public static Collection validCodesProviderMethod() {
Object[][] validCodes = new Object[][] { { "ar" }, { "us" } };
return Arrays.asList(validCodes);
}
@Test
public void validateAValidCodeShouldReturnTrue() {
CountryIsoCodeValidator validator = new CountryIsoCodeValidator();
Assert.assertTrue(validator.validate(this.code));
}
}
public class CountryIsoCodeValidatorTest {
@Test(dataProvider = "validCodesProviderName")
public void testValidCode(final String code) {
CountryIsoCodeValidator validator = new CountryIsoCodeValidator();
Assert.assertTrue(validator.validate(code));
}
@DataProvider(name = "validCodesProviderName")
public Object[][] validCodesProviderMethod() {
return new Object[][] { { "ar" }, { "us" } };
}
}
public class CountryIsoCodeValidatorTest2 {
@Test
@Parameters(value = "code")
public void validateAValidCodeShouldReturnTrue(final String code) {
CountryIsoCodeValidator validator = new CountryIsoCodeValidator();
Assert.assertTrue(validator.validate(code));
}
}
El testng.xml
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd"> <suite name="CountryIsoCodeValidatorTest2"> <test name="validate-ar"> <parameterfont-size:10.0pt;font-family:"Courier New";color:teal">ode" value="ar" /> <classes> <class name="com.hexacta.hat.testingframeworks.testng.CountryIsoCodeValidatorTest2" /> </classes> </test> <test> <parameter name="code" value="us" /> <classes> <class name="com.hexacta.hat.testingframeworks.testng.CountryIsoCodeValidatorTest2" /> </classes> </test> </suite>
En este punto las diferencias son más notables. TestNG aparece más flexible que JUnit, ya que en JUnit tenemos que usar un método estático para proveer los parámetros y estamos obligados a tener una variable de instancia junto con el constructor para instanciarla y luego el test va a hacer uso de esa variable de instancia. En este caso, todos los métodos de la clase de test van a correr tantas veces como cantidad de elementos tenga el arreglo Object[cantidad_de_datos][cantidad_de_parametros] y depende de nosotros usar el parámetro en el test.
En el caso de TestNG tenemos dos opciones para hacer lo mismo:
En cualquiera de las dos opciones que ofrece TestNG los paramétros son para el test case que queramos dentro de la clase. A diferencia de JUnit, que como se menciona en el párrafo anterior estamos restringiendo toda la clase de test a que use el método que provee parámetros.
Otra diferencia es el output en Eclipse. Como se puede observar en las siguientes imágenes, TestNG nuestra que test se corrió con que parámetro (haciendo un toString del parámtro dado), mientras que JUnit solo indica la cantidad de veces que corrió el test.
JUnit
En TestNG podemos declarar dependencias entre tests. En el caso de tests funcionales esta funcionalidad puede ser de gran ayuda. Supongamos que tenemos una aplicación web que queremos testear en un entorno en particular. Podríamos tener una batería test inmensa con un tests para el login de un usuario y luego varios más, por ejemplo, uno para ver la información de la cuenta del usuario. En este caso, si el primer test falla, no nos interesaría que se ejecuten el resto de los test, ya que van a fallar porque dependen del primero. Con el uso de dependencia de tests, el resto de los test no se correrían sino que se saltearína “skip”. El concepto de test salteado “skipped” es propio de TestNG y podemos verlo en las imágenes que están más arriba.
public class AccountHistoryTest {
@Test
public void verifyLogIn() {
...
}
@Test(dependsOnMethods = { "verifyLogIn" })
public void verifyAccountInfo() {
...
}
}
Before / After
Ambos frameworks proven annotations para el setup y clean up de la ejecución de los tests. En JUnit contamos con las annotations: @Before, @BeforeClass, @After, @AfterClass. Las cuatro pueden ser usadas en las clases de tests. En las suites podemos usar las @BeforeClass y @AfterClass, que se ejecutarían antes de que se ejecute el primer test de la suite, o después del último.
En TestNG las annontations son @BeforeMethod, @BeforeTest, @BeforeClass, @BeforeSuite, @BeforeGroups, @AfterMethod, @AfterTest, @AfterClass, @AfterSuite y @AfterGropus.
El mapeo simple de ambos sería:
| JUnit | TestNG |
| @Before / @After | @BeforeMethod /@AfterMethod |
| @BeforeClass / @AfterClass (en clase de test) | @BeforeClass / @AfterClass |
| @BeforeClass / @AfterClass (en clase de suite) | @BeforeSuite / @AfterSuite |
TestNG agrega el @BeforeTest / @AfterTest y el @BeforeGroup / @AfterGroup.
El @BeforeTest / @AfterTest se corre antes de un “<test>” de la suite (ver terminología dentro de suites) y después de que corran todos los métodos de test dentro del “<test>”.
El @BeforeGroup / @AfterGroup se corre antes de que el primer test dentro de un grupo se corra y después del último.
Grupos de tests
El annotation @Test deTestNG tiene un attributo llamado “group”, donde podemos declarar que dicho método de test pertenece a uno o varios grupos. Una vez definidos estos grupos podemos incluir / excluir grupos de la corrida (por medio del testng.xml).
Supongamos que estamos testeando un servicio web, podemos tener un test para levantar el servicio, otros para configurarlo y otros para testear cierta funcionalidad base X y varios más que testeen todo más a fondo. En este caso podríamos tener 4 grupos distintos:
Init / configure / basic / aditional
Dado esta configuración de tests podríamos hacer que el grupo configure dependa de init y tanto basic como aditional de configure. Además podría darse el caso que los tests aditional lleven mucho tiempo en correr, entonces cuando estamemos desarrollando podríamos correr una suite que incluya solamente los tests basic y deje de lado los aditional y correr la suite completa todas las noches.
Eclipse / IntelliJ IDEA: Los runners de JUnit como de TestNG vienen ya instalados en el IntelliJ IDEA (Community Edition), pero en Eclipse solamente está JUnit, y para correr los test de TestNG necesitamos instalar el plug-in.
MAVEN: Maven 2 no requiere ningún tipo de configuración especial para correr tanto los tests de JUnit como los de TestNG.
Varios proyectos arrancaron con JUnit 3 y no se migraron por diferentes razones, migrar desde JUnit 3 a JUnit 4 es muy fácil. Lo primero es cambiar la dependencia, pero lo bueno es que esto no nos rompe los tests que tenemos, ya que mantiene compatibilidad hacia atrás. Luego habría que romper la herencia con TestBase y migrar realmente. Esto con TestNG no es tan sencillo, ya que al cambiar la dependencia empieza a fallar.
Bamboo
Esto es para los usuarios de Bamboo. Hay un bug conocido en Bamboo en el reporte de tests parametrizados cuando se usa TestNG. Bamboo reporta las corridas de test, separando cada test parametrizado en n tests (cuando tiene n parámetros). Este reporte no funciona del todo bien con TestNG. Bamboo muestra que un test corrió (en vez de n) y en caso de que falle el test para alguno de los paramámetros muestra como que un test fue satisfactorio y a su vez que un test falló (en vez de n fallidos y m satisfactorios).