Comprender las 4 herramientas principales que utilizaras en todas sus pruebas unitarias.
Puedes odiar o amar las pruebas unitarias, eso depende de ti, pero el hecho es que si no entiendes los conceptos detrás de ellas, serás tan eficiente escribiendo pruebas como yo cocinando y manteniendo la cocina limpia al mismo tiempo 😂 (alerta de spoiler: siempre hago un desastre).
El primer paso fundamental para ser bueno escribiendo pruebas unitarias es entender su enfoque. Las pruebas unitarias no son pruebas de integración, y tienen que probar una sola unidad de código, sea lo que sea que eso signifique.
Echemos un vistazo a 4 herramientas que usarás mientras escribes tus pruebas unitarias. Y no me refiero a tu IDE ni a ningún plugin o extensión, sino a herramientas conceptuales: stubs, mocks, spies y dummies.
¡Vamos a ponernos manos a la obra!
¿Qué son los Stubs?
¿Recuerdas cuando dije que las pruebas unitarias no eran pruebas de integración? Bueno, ¡lo decía en serio!
A menudo veo a los desarrolladores escribir pruebas para el código que interactúa con una base de datos arrancando una "base de datos de prueba" dónde la prueba puede desencadenar una escritura (por ejemplo) y validarla consultando la BD. Esto es tan erróneo que tendría que dedicar un artículo entero para explicarlo en detalle, así que mientras tanto, toma mi palabra.
Los stubs te ayudan a lidiar con estas situaciones en las que tu código interactúa con servicios de terceros. Ya sea una base de datos, una API o incluso un archivo en tu disco duro, los stubs proporcionan al código que utiliza los servicios una versión más sencilla.
Esta nueva versión (el stub), en cambio, devuelve un valor conocido y controlado. Por ejemplo, si estás probando una función que escribe un valor en la base de datos, debes escribir un stub que evite la interacción con la base de datos pero que devuelva un resultado exitoso.
A través de ese podrás probar lo qué ocurre cuando la operación de escritura funciona. Luego puedes escribir otro stub (en otra prueba) que devuelva un resultado fallido, permitiéndote así probar la parte de tu lógica que se ocupa del manejo de errores.
Puedes hacer un stub de una función o un método en un objeto concreto (siempre que el lenguaje lo permita).
Veamos un ejemplo rápido.
/// the function to test
function saveUser(usrData, dbConn) {
let q = createQueryFromUser(usrData)
let result = dbConn.query(q)
return result;
}
//the stub
makeStub(dbConn, 'query', () => {
return true;
})
//the test
it("should return TRUE when the query succeeds", () => {
let result = saveUser({
name: "Fernando",
password: "1234"
}, dbConn)
result.should.be.true
})
El ejemplo anterior tiene algunas cosas que desentrañar, también fíjate que aunque el ejemplo está escrito en JavaScript, los conceptos se pueden extrapolar a todos los lenguajes.
Lo primero es la función a probar, ahora mismo es una simple función que recibe datos, un objeto de conexión a la base de datos y se apoya en un falso createQueryFromUser
para crear la consulta SQL real. El método query
desde el dbConn
es el que interactúa con la base de datos, y es el que nos interesa bloquear ya que no queremos que la consulta se dispare.
Aquí es donde entra en juego el talón, el makeStub
se encarga de sobrescribir mágicamente el método query
de nuestra conexión a la base de datos con la función anónima que estamos pasando (que es una función ficticia que sólo devuelve TRUE
cada vez)
Por último, la prueba unitaria real está haciendo uso del stub (porque fue definido antes). Esta prueba se asegura de que nuestra función devuelve el valor booleano correcto cuando las cosas van bien.
Este es sólo un ejemplo en el que puedes beneficiarte de los stubs. Para ser honesto, cada vez que tengas una función con un resultado dinámico, tendrás que encontrar una manera de asegurar el mismo resultado en cada ejecución de la prueba. Y para ello utilizarás los stubs.
¿Qué son los Mocks?
Los Mocks son como los hermanos gemelos de los Stubs, se parecen mucho y la gente suele confundirlos. Sin embargo, son dos individuos muy diferentes...err, o más bien, herramientas que puedes usar en tus pruebas.
Cuando los stubs te permitían reemplazar o redefinir una función o un método, los Mocks te permiten establecer comportamientos esperados en objetos/funciones reales. Así que técnicamente no estás reemplazando el objeto o la función, sólo le estás diciendo qué hacer en algunos casos muy específicos, aparte de eso, el objeto sigue funcionando como siempre.
Veamos un ejemplo para entender la definición: imagina que tienes que probar una función de reposición de pasillos. Ésta toma elementos del inventario y los coloca en el pasillo correspondiente. La clave que hay que probar aquí es que cada vez que reponemos un pasillo, también hay que tomar la misma cantidad de elementos del inventario.
Así que la expectativa aquí sería que cada vez que llamamos al método que se ocupa del contenido del pasillo, también llama al que se ocupa del inventario general y elimina la misma cantidad de elementos.
var inventory = createMock(Inventory("groceries"))
//set expectations
inventory.expect("getItems", 10).returns(TRUE).expect("removeFromInventory", 10).returns(TRUE)
var aisle = Aisle("groceries")
aisle.replenish(10, inventory) //executes the normal flow
assertion(aisle.isFull(), "equals to", TRUE)
Ten en cuenta que en algunos casos el comportamiento esperado para los mocks es comprobado automáticamente por el framework que estés utilizando. La razón por la que no hay una aserción real que se ocupe de las expectativas, si no se cumplieran, el mock habría lanzado una excepción y la prueba no habría pasado.
Y en este caso particular, la expectativa dice que el getItems
con un 10 como atributo, devolverá TRUE
y que también llamará al removeFromInventory
con un 10 como atributo también. Terminando todo en un TRUE
que se devuelve.
Por supuesto, podríamos haber hecho esto usando stubs, pero ese no es el punto, en muchas situaciones estas herramientas pueden ser usadas para los mismos o similares casos de uso.
¿Qué son los Spies?
No, no, no, no estoy hablando de espías tipo 007, aquí seguimos hablando de pruebas unitarias, siento decepcionar 😂
Los Spies, como su nombre indica, nos permiten entender lo que sucede dentro del código probado, incluso cuando no tenemos realmente acceso a él. Suena sospechoso, lo sé, pero tiene su utilidad.
En otras palabras, los espías son stubs que recogen información de la ejecución, para poder decir, al final, qué se llamó, cuándo y con qué parámetros.
Piensa en el ejemplo de los mocks anterior, tuvimos que establecer las expectativas de antemano para asegurarnos de que todo lo que queríamos se ejecutaría. Podríamos comprobar lo mismo con los Spies, "espiando" el inventario y preguntando si esos métodos fueron realmente llamados y con qué parámetros.
Comprobemos esta vez otro ejemplo, una función lectora de archivos que también debe cerrar el manejador de archivos una vez que haya terminado con él.
const filename = "yourfile.txt"
let myspy = new Spy(IOModule, "closeFile") //create a spy for the method closeFile in the module dedicated to I/
function readConfigFile(fname) {
const reader = new FileReader(filename, IOModule)
let content = reader.read()
loadConfig(content)
IOModule.closeFile(reader);
}
//The test
it("should call the 'closeFile' method after reading the content of the file", () => {
readConfigFile(filename)
assertion(myspy.called, "equals to", TRUE)
})
La función a probar se llama readConfigFile
, está pensado para leer un archivo y cargar su contenido como configuración llamando a la función loadConfig
Como parte de nuestra prueba, nos interesa entender si la función realmente cierra el manejador de archivos.
Hay que tener en cuenta que esta prueba va en contra de lo que he dicho anteriormente porque realmente está abriendo y leyendo el archivo, que es una dependencia de terceros que nuestras pruebas unitarias no deberían tener. Para hacer esta prueba totalmente "compatible" tendríamos que añadir también un stub para nuestro IOModule
y controlar cuando nos interesa probar una lectura exitosa y una fallida.
Nota: Los Spies, a diferencia de los stubs, envuelven el método/función objetivo en lugar de reemplazarlo, por lo que el código original de su objetivo también se ejecutará.
¿Qué son los Dummies?
Finalmente, la última herramienta que quiero cubrir es la famosa e inútil "dummies". Los dummies, como su nombre indica, son simples objetos que no sirven para nada más que para estar ahí cuando se necesitan. Su propósito es estar ahí cuando la sintaxis lo requiera.
Por ejemplo, imagina que tiene que llamar a una función que toma 3 argumentos, siendo el primero otra función (una dependencia externa). Dado el stub actual para esa función, sabes que los otros 2 atributos no se utilizarán, sin embargo, el intérprete/compilador se queja de que te faltan los 2 últimos atributos de la función, por lo que necesitas añadirlos.
¿Cómo se puede hacer eso?
Lo has adivinado, a través de los dummies. Simplemente añadirás 2 objetos ficticios que no hacen nada pero que son aceptados por el compilador.
Los dummies tienen más sentido cuando se usan dentro de lenguajes fuertemente tipados porque este tipo de comprobaciones son más comunes allí. Por ejemplo, echa un vistazo al siguiente ejemplo de TypeScript:
type UserData = {
name: string;
password: string
}
//The function to be tested
function saveUser(usrData: UserData, dbConn: DataBase, validators:DataValidators) {
if(!validators.validateUserData(usrData)) {
return false;
}
let query = createQueryFromData(usrData);
let result = dbConn.query(query);
return result;
}
// The test itself
//the stub
const stubbedValidators: DataValidators = {
validateUserData: (data: UserData) => false;
}
//the dummies
const userData: UserData = {name: "", password: ""}
const dbConn: DataBase = {}
//the test
it("should return false if the user data is not valid", () => {
let result = saveUser(userData, dbConn, stubbedValidators);
result.should.be.false;
})
El código define nueva función saveUser
que también toma un validators
dependencia. También hemos añadido un paso de validación, para asegurarnos de que los datos que intentamos guardar son "válidos" (sea lo que sea que eso signifique).
Pero la intención de nuestra prueba es asegurarnos de que si los datos no son válidos, estamos devolviendo false
Esto significa que realmente no estamos realizando ninguna validación, de hecho, necesitamos hacer un stub de ese validador para controlar el resultado, de lo contrario si mañana nuestra rutina de validación cambiara, podríamos estar pasando ahora una muestra de datos válida y el test fallaría.
El problema ahora es que mirando nuestra lógica de negocio si los datos no son válidos, no estamos realmente usando la conexión de la base de datos, ni los datos reales del usuario. Necesitamos que estén ahí, pero no los necesitamos realmente. Así que efectivamente se han convertido en maniquíes.
Por eso estoy pasando objetos vacíos falsos (A.K.A dummies) como los primeros 2 atributos de la función.
Conclusión
Los Stubs, los Mocks, los Spies y los Dummies son el pan de cada día en tus pruebas, cuanto más los uses, más familiares te parecerán y más fácil te resultará entender cómo afrontar una nueva prueba.