Patricio López Juri (1263476J) (pelopez2@uc.cl)
- La razón de cada test está puesto en los
.java
sobre el respectivo test.
-
Probando el método
isAlive
se descubrió queisAlive
retornabatrue
cuando la celda a consultar se salía de los bordes horizontales. Se corrigió afalse
. -
Se descubrió que el método
setCell
tenía dos errores (ambos corregidos):- Asignaba mal la variable
i
de largo cuando se salía horizontalmente. - No asignaba realmente el valor.
- Asignaba mal la variable
- Se descubrió que la regla de nacer estaba mal puesta. Se corrigió a
3 OR 6
.
- Se descubrió un error booleano. Se corrió de
AND
aOR
. - Se descubrió que
countAliveNeighbors
se contaba a sí mismo cuando se le preguntaba por los vecinos. Se corrigió para que no lo cuente.
Dado un set de requerimientos y/o reglas del software es fácil crear test unitarios que validen la correctitud de las características a probar.
Así también al largo plazo el costo de corregir, mantener y mejorar el software es más barato y menos propenso a errores. Un desarrollador o incluso alguien no tan familiarizado con la programación puede leer los encabezados o descripciones de los test unitarios y entender qué están verificando.
Por ejemplo, es sencillo entender que esto verifica una regla del juego:
/**
* Test born rule according to HighLife rules.
* "... a cell is born if it has 3 or 6 neighbors."
*/
@Test
public void testBornRule() {
// ...
}
Por otro lado, si es necesario hacer un cambio en el siguiente fragmento, podemos hacer un test de regresión para comprobar que no rompimos nada anterior:
/**
* To check if, at the next iteration, the specified cell, if dead,
* should be reborn.
*/
public boolean shouldBeBorn(int i, int j) {
// ...
}
En primer lugar, los test unitarios no verifican el comportamiento del componente en un sistema con más componentes. Es necesario realizar test de integración una vez que pasan los test-unitarios.
El gran costo de depender de un sistema aparte es al momento de realizar los test-unitarios que dependen de la interacción con ese otro sistema. Esto porque es necesario hacer mocks o simularlo de algún modo, lo que es bastante costoso en términos de horas hombre.
A un nivel más micro y en base a este juego, podemos pensar a la GUI como un componente externo. Para realizar un testing más intensivo en el proyecto deberíamos:
- Testear (unitariamente) las reglas del juego por si solas
- Realizar un mock de la interfaz gráfica para poder testear el juego y su interacción con una API gráfica, aunque sea "de mentiras".
- Realizar test unitarios de la interfaz gráfica
- Ej: Se marca un cuadrado al llamar cierto método.
- Ej: Se crea con los dimensiones de pixeles definidas, etc.
- Realizar un mock de la lógica para probarlo con la interfaz.
- Finalmente, configurar test de integración donde probemos a la lógica funcionando con la interfaz.
Lo mencionado anteriormente requiere aún más lineas de código y trabajo que el MVP del software en sí.
En segundo lugar, los test unitarios dificultan cuando existen segmentos que dependen de variables aleatorias. Esto porque el output del programa se vuelve no determinista y se requiere:
- Configurar una seed con tal de que las funciones random sean deterministas.
- Correr el test múltiples veces.
Por ejemplo en el fragmento:
public HighLifeBoard(int length, int width, boolean random){
// ...
if(random) {
for(int i = 0; i < length; i++)
for(int j = 0; j < width; j++) {
// 30% chance of being alive
this.board[i][j] = Math.random() > 0.7 ? true : false; // <- AQUÍ
}
}
}
La primera opción puede traer problemas dificultando la cobertura del test, la segunda es muy ingenua y puede traer problemas (y costos) al tener sistemas de CI.