One place for hosting & domains

      probar

      Cómo probar un módulo de Node.js con Mocha y Assert


      El autor seleccionó a Open Internet/Free Speech Fund para recibir una donación como parte del programa Write for DOnations.

      Introducción

      Las pruebas son una parte integral del desarrollo de software. Es común para los programadores ejecutar código para probar su aplicación a medida que realizan cambios y así confirmar que se se comporta como se espera. Con la configuración de prueba correcta, este proceso puede incluso automatizarse y ahorrar mucho tiempo. Ejecutar pruebas de manera uniforme tras escribir nuevo código garantiza que los nuevos cambios no afecten las funciones preexistentes. Esto proporciona al desarrollador confianza en su base de código, en especial cuando se implementa en la producción para que los usuarios puedan interactuar con ella.

      Un marco de prueba estructura la forma en que creamos casos de prueba. Mocha es un marco de prueba de JavaScript muy popular que organiza nuestros casos de prueba y se encarga de ejecutarlos. Sin embargo, no verifica el comportamiento de nuestro código. Para comparar los valores en una prueba, podemos usar el módulo assert de Node.js.

      En este artículo, escribirá pruebas para un módulo de lista TODO de Node.js. Configurará y usará el marco de prueba Mocha para estructurar sus pruebas. A continuación, usará el módulo assert Node.js para crear las pruebas propiamente dichas. En este sentido, usará Mocha como creador del plan, y assert para implementar el plan.

      Requisitos previos

      Paso 1: Escribir un módulo Node

      Nuestro primer paso para este artículo será escribir el módulo Node.js que nos gustaría probar. Este módulo administrará una lista de elementos TODO. Con este módulo podremos enumerar todos los elementos TODO de los que realizamos un seguimiento, añadir nuevos elementos y marcar algunos como completados. Además, podrá exportar una lista de elementos TODO a un archivo CSV. Si desea repasar la manera de escribir módulos de Node.js, puede leer nuestro artículo Cómo crear un módulo de Node.js.

      Primero, debemos configurar el entorno de codificación. Cree una carpeta con el nombre de su proyecto en su terminal. En este tutorial se usará el nombre todos:

      Luego acceda a esa carpeta:

      Ahora, inicie npm. Usaremos su funcionalidad CLI para ejecutar las pruebas más tarde:

      Solo tenemos una dependencia, Mocha, que usaremos para organizar y ejecutar nuestras pruebas. Para descargar e instalar Mocha, utilice lo siguiente:

      • npm i request --save-dev mocha

      Instalamos Mocha como dependencia dev, ya que en un entorno de producción el módulo no lo necesita. Si desea obtener más información sobre los paquetes de Node.js o npm, consulte nuestra guía Cómo usar módulos Node.js con npm y package.json.

      Finalmente, crearemos nuestro archivo que contendrá el código de nuestro módulo:

      Con esto, estaremos listos para crear nuestro módulo. Abra index.js en un editor de texto; por ejemplo, nano:

      Comenzaremos definiendo la clase Todos. Esta clase contiene todas las funciones que necesitamos para administrar nuestra lista TODO. Añada las siguientes líneas de código a index.js:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      }
      
      module.exports = Todos;
      

      Comenzamos el archivo creando una clase Todos. Su función constructor() no toma argumentos. Por lo tanto, no es necesario proporcionar valores para crear instancias de un objeto para esta clase. Lo único que hacemos cuando iniciamos un objeto Todos es crear una propiedad todos, que es una matriz vacía.

      La línea modules permite que otros módulos de Node.js requieran nuestra clase Todos. Si no se exporta explícitamente la clase, el archivo de prueba que crearemos más tarde no podrá usarla.

      Añadiremos una función para mostrar la matriz de todos que almacenamos. Escriba las siguientes líneas resaltadas:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      }
      
      module.exports = Todos;
      

      Nuestra función list() devuelve una copia de la matriz usada por la clase. Hace una copia de la matriz usando la sintaxis de desestructuración de JavaScript. Creamos una copia de la matriz de modo que los cambios que el usuario realice en la matriz y se muestren a través de list() no afecten a la matriz usada por el objeto Todos.

      Nota: Las matrices de JavaScript son tipos de referencia. Esto significa que para cualquier asignación de variable a una matriz o invocación de función con una matriz como parámetro, JavaScript consulta la matriz original que se creó. Por ejemplo, si tenemos una matriz con tres elementos llamada x, y creamos una nueva variable y de modo que y = x, y y x hacen referencia a lo mismo. Cualquier cambio que realicemos a la matriz con y afecta a la variable x y viceversa.

      Ahora escribiremos la función add(), que añade un nuevo elemento TODO:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      }
      
      module.exports = Todos;
      

      Nuestra función add() toma una cadena y la dispone en una nueva propiedad title del objeto de JavaScript. El nuevo objeto también tiene una propiedad completed, que se fija en false por defecto. Luego añadimos este nuevo objeto a nuestra matriz de TODO.

      Una funcionalidad importante en un administrador de TODO es la de marcar los elementos como completados. Para esta implementación, repetiremos nuestra matriz todos para hallar el elemento TODO que el usuario está buscando. Si se encuentra uno, lo marcaremos como completado. Si no se encuentra ninguno, mostraremos un error.

      Añada la función complete() de esta forma:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      
          complete(title) {
              let todoFound = false;
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      }
      
      module.exports = Todos;
      

      Guarde el archivo y cierre el editor de texto.

      Ahora disponemos de un administrador TODO básico con el que podemos experimentar. A continuación, probaremos nuestro código manualmente para ver si la aplicación funciona.

      Paso 2: Probar el código de forma manual

      En este paso, ejecutaremos las funciones de nuestro código y observaremos el resultado para garantizar que cumpla con nuestras expectativas. Esto se conoce como prueba manual. Es probablemente la metodología de prueba más común que aplican los programadores. Aunque automatizaremos nuestra prueba más tarde con Mocha, primero probaremos manualmente nuestro código para dar un mejor sentido a cómo la prueba manual difiere de los marcos de prueba.

      Añadiremos dos elementos TODO a nuestra aplicación y marcaremos uno como completado. Inicie el REPL de Node.js en la misma carpeta que el archivo index.js:

      Verá el símbolo > en el REPL que dice que podemos ingresar código de JavaScript. Escriba lo siguiente en el símbolo:

      • const Todos = require('./index');

      Con require(), cargamos el módulo TODOs en una variable Todos. Recuerde que nuestro módulo muestra la clase Todos por defecto.

      Ahora, vamos a instanciar un objeto para esa clase. En el REPL, añada esta línea de código:

      • const todos = new Todos();

      Podemos usar el objeto todos para verificar que nuestra implementación funcione. Añadiremos nuestro primer elemento TODO:

      Hasta ahora, no vimos ningún resultado en nuestro terminal. Verificaremos que almacenamos nuestro elemento TODO "run code" obteniendo una lista de todos nuestros TODO:

      Verá este resultado en su REPL:

      Output

      [ { title: 'run code', completed: false } ]

      Este es el resultado esperado: tenemos un elemento TODO en nuestra matriz de TODO y no está completado por defecto.

      Vamos añadiremos otro elemento TODO:

      • todos.add("test everything");

      Marque el primer elemento TODO como completado:

      • todos.complete("run code");

      Nuestro objeto todos ahora estará administrando dos elementos: "run code" y "test everything". El TODO "run code" también se completará. Confirmaremos esto invocando list() de nuevo:

      El REPL mostrará el siguiente resultado:

      Output

      [ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]

      Ahora, salga del REPL con lo siguiente:

      Hemos confirmado que nuestro módulo se comporta como esperamos. Aunque no dispusimos nuestro código en un archivo de prueba o usamos una biblioteca de prueba, probamos nuestro código manualmente. Desafortunadamente, esta forma de prueba requiere mucho tiempo cada vez que se realiza un cambio. A continuación, utilizaremos las pruebas automatizadas en Node.js y veremos si podemos resolver este problema con el marco de prueba Mocha.

      Paso 3: Escribir su primera prueba con Mocha y Assert

      En el último paso, probamos nuestra aplicación de forma manual. Esto funcionará para los casos de uso individuales, pero a medida que nuestro módulo se amplía, este método se vuelve menos viable. A medida que probemos nuevas funciones, debemos estar seguros de que la funcionalidad añadida no genere problemas con la funcionalidad anterior. Nos gustaría probar cada función una y otra vez para cada cambio del código, pero hacer esto a mano tomaría mucho esfuerzo y podría producir a errores.

      Una práctica más eficiente consistiría en configurar pruebas automatizadas. Estas son pruebas con secuencias de comandos escritas como cualquier otro bloque de código. Ejecutamos nuestras funciones con entradas definidas e inspeccionamos sus efectos para garantizar que se comporten como esperamos. A medida que nuestra base de código se amplíe, lo mismo sucederá con nuestras pruebas automatizadas. Cuando escribimos nuevas pruebas junto a las funciones, podemos verificar que el módulo completo aún funcione, todo sin necesidad de recordar la manera en que se usa cada función en cada ocasión.

      En este tutorial, usaremos el marco de pruebas Mocha con el módulo assert de Node.js. Recurriremos a la experiencia práctica para ver cómo funcionan juntos.

      Para comenzar, cree un nuevo archivo para almacenar nuestro código de prueba:

      Ahora, utilice su editor de texto para abrir el archivo de prueba. Puede usar nano como antes:

      En la primera línea del archivo de texto, cargaremos el módulo TODO como hicimos en el shell de Node.js. Luego cargaremos el módulo assert para cuando escribamos nuestras pruebas. Añada las siguientes líneas:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      

      La propiedad strict del módulo assert nos permitirá usar las pruebas de igualdad especiales que se recomiendan desde Node.js y son adecuadas para una buena protección futura, ya que tienen en cuenta la mayoría de los casos de uso.

      Antes de pasar a las pruebas de escritura, veremos cómo Mocha organiza nuestro código. Las pruebas estructuradas en Mocha normalmente siguen esta plantilla:

      describe([String with Test Group Name], function() {
          it([String with Test Name], function() {
              [Test Code]
          });
      });
      

      Observe las dos funciones principales: describe() e it(). La función describe() se usa para agrupar pruebas similares. No es necesario para que Mocha ejecute las pruebas, pero agrupar las pruebas facilita el mantenimiento de nuestro código. Se recomienda que agrupe sus pruebas de una manera que le permita actualizar fácilmente las que son similares.

      it() contiene nuestro código de prueba. Aquí interactuaremos con las funciones de nuestro módulo y usaremos la bibliteca assert. Muchas funciones it() pueden definirse en una función describe().

      Nuestro objetivo en esta sección es usar Mocha y assert para automatizar nuestra prueba manual. Haremos esto paso a paso, comenzando con nuestro bloque de descripción. Añada lo siguiente a su archivo después de las líneas del módulo:

      todos/index.test.js

      ...
      describe("integration test", function() {
      });
      

      Con este bloque de código, hemos creado una agrupación para nuestras pruebas integradas. Las pruebas Unit probarán una función a la vez. Las pruebas de integración verifican si las funciones en o entre los módulos funcionan bien juntas. Cuando Mocha ejecute nuestras pruebas, todas las pruebas en ese bloque de descripción se ejecutarán en el grupo "integration test".

      Añadiremos una función it() para que podamos comenzar a probar el código de nuestro módulo.

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
          });
      });
      

      Observe que hicimos que el nombre de la prueba sea descriptivo. Si alguien ejecuta nuestra prueba, quedará claro de inmediato qué supera la prueba y qué no. Una aplicación bien probada normalmente es una aplicación bien documentada, y las pruebas pueden ser a veces un tipo de documentación efectivo.

      Para nuestra primera prueba, crearemos un nuevo objeto Todos y verificaremos que no tenga elementos.

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      La primera nueva línea de código creó una instancia de un nuevo objeto Todos como haríamos en el REPL de Node.js o en otro módulo. En la segunda nueva línea, usamos el módulo assert.

      Desde el módulo assert usamos el método notStrictEqual(). Esta función toma dos parámetros: el valor que deseamos probar (llamado actual) y el valor que esperamos obtener (llamado expected). Si ambos argumentos son iguales, notStrictEqual() genera un error para que la prueba falle.

      Guarde y cierre index.test.js.

      El caso básico será true, ya que la extensión debería ser 0, que no es 1. Vamos a confirmar esto ejecutando Mocha. Para hacer esto, debemos modificar nuestro archivo package.json. Abra el archivo package.json con su editor de texto:

      Ahora, en su propiedad scripts, cámbielo para que tenga este aspecto:

      todos/package.json

      ...
      "scripts": {
          "test": "mocha index.test.js"
      },
      ...
      

      Acabamos de cambiar el comportamiento del comando CLI test de npm. Cuando ejecutemos npm test, npm revisará el comando que acabamos de introducir en package.json. Buscará la biblioteca Mocha en nuestra carpeta node_modules y ejecutará el comando mocha con nuestro archivo de prueba.

      Guarde y cierre package.json.

      Veamos qué sucede cuando ejecutemos nuestra prueba. En su terminal, introduzca lo siguiente:

      El comando producirá el siguiente resultado:

      Output

      > [email protected] test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

      Este resultado primero nos muestra el grupo de pruebas que está a punto de ejecutarse. Para cada prueba individual en un grupo, se marca el caso de prueba. Vemos el nombre de nuestra prueba como lo describimos en la función it(). La marca de verificación en el lado izquierdo del caso de prueba indica que se superó la prueba.

      En la parte inferior, vemos un resumen de todas nuestras pruebas. En nuestro caso, nuestra prueba fue superada y se completó en 16 ms (el tiempo varía según el equipo).

      Nuestra prueba se inició correctamente. Sin embargo, este caso de prueba actual puede dar lugar a instancias de “falsos positivos”. Un falso positivo es un caso en el que se supera la prueba cuando esto no debería suceder.

      Actualmente comprobamos que la extensión de la matriz no sea igual a 1. Modificaremos la prueba de modo que esta condición sea “true” cuando este no debería ser el caso. Añada las siguientes líneas a index.test.js:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Guarde el archivo y ciérrelo.

      Añadimos dos elementos TODO. Ejecutaremos la prueba para ver qué sucede:

      Esto dará el siguiente resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)

      La prueba se supera, como se espera, ya que la extensión es mayor a 1. Sin embargo, se frustra la finalidad original de contar con esa primera prueba. La primera prueba está destinada a confirmar que empezamos en un estado vacío. Una mejor prueba confirmará eso en todos los casos.

      Cambiaremos la prueba de modo que solo se supere si no tenemos ningú´n TODO. Aplique los siguientes cambios en index.test.js:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Cambió notStrictEqual() por strictEqual(), una función que comprueba la igualdad entre su argumento real y esperado. “strictEqual” fallará si nuestros argumentos no son exactamente los mismos.

      Guarde y cierre el archivo, y ejecute la prueba para que podamos ver lo que sucede:

      Esta vez, en el resultado se mostará un error:

      Output

      ... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.

      Este texto nos resultará útil para determinar por qué falló la prueba mediante depuración. Observe que, dado que la prueba falló, no apareció una marca de verificación al principio del caso de prueba.

      Nuestro resumen de prueba ya no está en la parte inferior del resultado, sino justo después de donde nuestra lista de casos de prueba se mostraba:

      ...
      0 passing (29ms)
        1 failing
      ...
      

      El resultado restante nos proporciona datos sobre las pruebas no superadas. Primero, vemos el caso de prueba que falló:

      ...
      1) integrated test
             should be able to add and complete TODOs:
      ...
      

      A continuación, vemos por qué falló nuestra prueba:

      ...
            AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
      + expected - actual
      
      - 2
      + 0
            + expected - actual
      
            -2
            +0
      
            at Context.<anonymous> (index.test.js:9:10)
      ...
      

      Se presenta un AssertionError cuando strictEqual() falla. Vemos que el valor expected, 0, es diferente el valor actual.

      A continuación vemos la línea en nuestro archivo de prueba en el que falla el código. En este caso, es la línea 10.

      Ahora, vimos que nuestra prueba fallará si esperamos valores incorrectos. Cambiaremos nuestro caso de prueba de nuevo a su valor correcto. Primero, abra el archivo:

      A continuación, elimine las líneas todos.add de modo que su código tenga el siguiente aspecto:

      todos/index.test.js

      ...
      describe("integration test", function () {
          it("should be able to add and complete TODOs", function () {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Guarde el archivo y ciérrelo.

      Ejecútelo una vez más para confirmar que supere la prueba sin posibles instancias de “falsos positivos”:

      El resultado será el siguiente:

      Output

      ... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)

      Con esto, mejoramos bastante la resistencia de nuestra prueba. Continuaremos con nuestra prueba de integración. El siguiente paso es añadir un nuevo elemento TODO a index.test.js:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
          });
      });
      

      Tras utilizar la función add(), confirmamos que ahora tenemos un TODO administrado por nuestro objeto todos con strictEqual(). Nuestra siguiente prueba confirma los datos en todos con deepStrictEqual(). La función deepStrictEqual() prueba de forma recursiva que nuestros objetos esperados y reales tienen las mismas propiedades. En este caso, prueba que las matrices que esperamos tengan un objeto JavaScript en ellas. Luego comprueba que sus objetos JavaScript tengan las mismas propiedades; es decir, que sus propiedades titles sean "run code" y sus propiedades completed sean false.

      A continuación, completaremos las pruebas restantes con estas dos comprobaciones de igualdad, según sea necesario, añadiendo las siguientes líneas resaltadas:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
      
              todos.add("test everything");
              assert.strictEqual(todos.list().length, 2);
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: false },
                      { title: "test everything", completed: false }
                  ]
              );
      
              todos.complete("run code");
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: true },
                      { title: "test everything", completed: false }
                  ]
          );
        });
      });
      

      Guarde el archivo y ciérrelo.

      Nuestra prueba ahora imita a nuestra prueba manual. Con estas pruebas mediante programación, no es necesario comprobar el resultado continuamente si nuestras pruebas son superadas cuando las ejecutamos. Normalmente, le convendrá probar todos los aspectos de uso para garantizar que el código se pruebe adecuadamente.

      Ejecutaremos nuestra prueba con npm test una vez más para obtener este resultado familiar:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)

      Con esto, habrá configurado una prueba integrada con el marco Mocha y la biblioteca assert.

      Consideraremos una situación en la que haya compartido nuestro módulo con otros desarrolladores y ahora nos den su opinión. Una buena parte de nuestros usuarios desearán que la función complete() muestre un error si no se añadieron TODO aún. Añadiremos esta funcionalidad en nuestra función complete().

      Abra index.js en su editor de texto:

      Añada lo siguiente a la función:

      todos/index.js

      ...
      complete(title) {
          if (this.todos.length === 0) {
              throw new Error("You have no TODOs stored. Why don't you add one first?");
          }
      
          let todoFound = false
          this.todos.forEach((todo) => {
              if (todo.title === title) {
                  todo.completed = true;
                  todoFound = true;
                  return;
              }
          });
      
          if (!todoFound) {
              throw new Error(`No TODO was found with the title: "${title}"`);
          }
      }
      ...
      

      Guarde el archivo y ciérrelo.

      Ahora, añadiremos una nueva prueba para esta nueva función. Queremos verificar que si invocamos “completed” en un objeto Todos que no tiene elementos, mostrará nuestro error especial.

      Vuelva a index.test.js:

      Al final del archivo, añada el siguiente código:

      todos/index.test.js

      ...
      describe("complete()", function() {
          it("should fail if there are no TODOs", function() {
              let todos = new Todos();
              const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");
      
              assert.throws(() => {
                  todos.complete("doesn't exist");
              }, expectedError);
          });
      });
      

      Usamos describe() y it() como antes. Nuestra prueba comienza con la creación de un nuevo objeto todos. A continuación, definimos el error que estamos esperando ver cuando invocamos la función complete().

      A continuación, usamos la función throws() del módulo assert. Esta función se creó para que podamos verificar los errores que se producen en nuestro código. Su primer argumento es una función que contiene el código que produce el error. El segundo argumento es el error que estamos esperando ver.

      En su terminal, ejecute las pruebas con npm test de nuevo. Ahora verá el siguiente resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)

      En este resultado se resalta el beneficio que nos trae realizar pruebas automatizadas con Mocha y assert. Debido a que nuestras pruebas están programadas, cada vez que ejecutamos npm test verificamos que todas nuestras pruebas se superan. No fue necesario comprobar manualmente si el otro código aún funciona; sabemos que es así porque las pruebas que tenemos se han superado.

      Hasta ahora, nuestras pruebas han verificado los resultados del código sincronizado. Vamos a ver cómo deberíamos adaptar nuestros nuevos hábitos de prueba para que funcionen con el código asíncrono.

      Paso 4: Probar código asíncrono

      Una de las funciones que deseamos en nuestro módulo TODO es una función de exportación de CSV. Imprimirá en un archivo todos los TODO que tenemos guardados junto con el estado “completed”. Esto requiere que usemos el módulo fs: un módulo de Node.js integrado para trabajar con el sistema de archivos.

      La escritura en un archivo es una operación asíncrona. Existen muchas formas de escribir en un archivo en Node.js. Podemos usar callbacks, promesas o las palabras claves async/await. En esta sección, veremos la manera de escribir pruebas para esos métodos diferentes.

      Callbacks

      Una función callback es la que se utiliza como argumento para una función asíncrona. Se invoca cuando se completa la operación asíncrona.

      Añadiremos una función a nuestra clase Todos llamada saveToFile(). Esta función creará una cadena realizando un bucle con todos nuestros elementos TODO y escribiendo esa cadena en un archivo.

      Abra su archivo index.js:

      En este archivo, añada el siguiente código resaltado:

      todos/index.js

      const fs = require('fs');
      
      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
              this.todos.push(todo);
          }
      
          complete(title) {
              if (this.todos.length === 0) {
                  throw new Error("You have no TODOs stored. Why don't you add one first?");
              }
      
              let todoFound = false
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      
          saveToFile(callback) {
              let fileContents = 'Title,Completedn';
              this.todos.forEach((todo) => {
                  fileContents += `${todo.title},${todo.completed}n`
              });
      
              fs.writeFile('todos.csv', fileContents, callback);
          }
      }
      
      module.exports = Todos;
      

      Primero tenemos que importar el módulo fs en nuestro archivo. Luego añadimos nuestra nueva función saveToFile(). Nuestra función toma una función callback que se utilizará una vez que se complete la operación de escritura del archivo. En esa función, creamos una variable fileContents que almacena toda la cadena que queremos guardar como archivo. Se inicializa con los encabezados de CSV. A continuación, realizamos un bucle con cada elemento TODO con el método forEach() de la matriz interna. A medida que realizamos iteraciones, añadimos las propiedades title y completed de los objetos todos individuales.

      Finalmente, usamos el módulo fs para escribir el archivo con la función writeFile(). Nuestro primer argumento es el nombre del archivo: todos.csv. El segundo es el contenido del archivo. En este caso, nuestra variable fileContents. Nuestro último argumento es nuestra función callback, que gestiona cualquier error de escritura.

      Guarde el archivo y ciérrelo.

      Ahora escribiremos una prueba para nuestra función saveToFile(). Nuestra prueba hará dos acciones: confirmar que el archivo existe en primer lugar y luego verificar que su contenido sea correcto.

      Abra el archivo index.test.js:

      Comenzaremos cargando el módulo fs en la parte superior del archivo, ya que lo usaremos para ayudar a probar nuestros resultados:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      const fs = require('fs');
      ...
      

      Ahora, al final del archivo, añadiremos nuestro caso de prueba:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function(done) {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.saveToFile((err) => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
                  done(err);
              });
          });
      });
      

      Como antes, usamos describe() para agrupar nuestras pruebas por separado respecto de las otras, ya que implica una nueva funcionalidad. La función it() se diferencia ligeramente de las otras. Normalmente, la función callback que usamos no tiene argumentos. Esta vez, tenemos done como argumento. Necesitamos este argumento cuando probemos funciones con callbacks. Mocha utiliza la función de callback done() para indicarle cuando se completa una función asíncrona.

      Todas las funciones de callback probadas en Mocha deben invocar el callback done(). Si no es así, Mocha nunca sabría cuándo se completó la función y se quedaría esperando una señal.

      Para continuar, creamos nuestra instancia de Todos y añadimos un único elemento a ella. Luego, invocamos la función saveToFile() con un callback que captura un error de escritura de archivo. Observe que nuestra prueba para esta función reside en el callback. Si nuestro código de prueba estuviera fuera del callback, fallaría siempre que se invocase antes de completar la escritura del archivo.

      En nuestra función de callback, primero comprobamos que nuestro archivo existe:

      todos/index.test.js

      ...
      assert.strictEqual(fs.existsSync('todos.csv'), true);
      ...
      

      La función fs.existsSync() muestra true si la ruta del archivo en su argumento existe; de lo contrario, será false.

      Nota: Las funciones del módulo fs son asíncronas por defecto. Sin embargo, para las funciones claves, crean equivalentes síncronos. Esta prueba es más sencilla al usar las funciones síncronas, ya que no necesitamos anidar el código asíncrono para garantizar que funcione. En el módulo fs, las funciones síncronas normalmente terminan con "Sync" al final de sus nombres.

      A continuación, crearemos una variable para guardar nuestro valor esperado:

      todos/index.test.js

      ...
      let expectedFileContents = "Title,Completednsave a CSV,falsen";
      ...
      

      Usamos readFileSync() del módulo fs para leer el archivo de forma síncrona:

      todos/index.test.js

      ...
      let content = fs.readFileSync("todos.csv").toString();
      ...
      

      Ahora, proporcionamos readFileSync() con la ruta correcta para el archivo: todos.csv. A medida que readFileSync() muestre el objeto Buffer, que almacena datos binarios, usamos su método toString() para poder comparar su valor con la cadena que esperamos tener guardada.

      Como antes, usamos el strictEqual del módulo assert para realizar una comparación:

      todos/index.test.js

      ...
      assert.strictEqual(content, expectedFileContents);
      ...
      

      Terminamos nuestra prueba invocando el callback done() y garantizamos que Mocha sepa detener la prueba de ese caso:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Proporcionamos el objeto err a done() de modo que Mocha pueda fallar en la prueba en caso de que se produzca un error.

      Guarde y cierre index.test.js.

      Ejecutaremos esta prueba con npm test como antes. En su consola se mostrará este resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)

      De esta manera, habrá probado su primera función asíncrona con Mocha usando callbacks. Sin embargo, en el momento en que se redactó este tutorial las promesas eran más prevalentes que los callbacks en el nuevo código Node.js, como se explica en nuestro artículo Cómo escribir código asíncrono en Node.js. A continuación, aprenderá a pobarlos con Mocha también.

      Promesas

      Una promesa es un objeto de JavaSript que, llegado el momento, mostrará un valor. Cuando una promesa se realiza correctamente, se resuelve. Cuando en ella se produce un error, se rechaza.

      Modificaremos la función saveToFile() para que utilice promesas en vez de callbacks. Abra index.js:

      Primero, debemos cambiar la forma en que se carga el módulo fs. En su archivo index.js, cambie la instrucción require() en la parte superior del archivo de modo que tenga este aspecto:

      todos/index.js

      ...
      const fs = require('fs').promises;
      ...
      

      Acabamos de importar el módulo fs, que usa promesas en vez de callbacks. Ahora, debemos realizar algunos cambios en saveToFile() para que funcione con promesas.

      En su editor de texto, aplique los siguientes cambios a la función saveToFile() para eliminar los callbacks:

      todos/index.js

      ...
      saveToFile() {
          let fileContents = 'Title,Completedn';
          this.todos.forEach((todo) => {
              fileContents += `${todo.title},${todo.completed}n`
          });
      
          return fs.writeFile('todos.csv', fileContents);
      }
      ...
      

      La primera diferencia es que nuestra función ya no acepta ningún argumento. Con promesas no necesitamos una función de callback. El segundo cambio hace referencia a la forma en que se escribe el archivo. Ahora, devolvemos el resultado de la promesa writeFile().

      Guarde y cierre index.js.

      Ahora adaptaremos nuestra prueba de modo que funcione con promesas. Abra index.test.js:

      Cambie la prueba saveToFile() por lo siguiente:

      todos/index.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function() {
              let todos = new Todos();
              todos.add("save a CSV");
              return todos.saveToFile().then(() => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
              });
          });
      });
      

      El primer cambio que debemos realizar es eliminar el callback done() de sus argumentos. Si Mocha pasa el argumento done(), debe invocarse. De lo contrario, generará un error como este:

      1) saveToFile()
             should save a single TODO:
           Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
            at listOnTimeout (internal/timers.js:536:17)
            at processTimers (internal/timers.js:480:7)
      

      Al probar promesas, no incluya el callback done() en it().

      Para probar nuestra promesa, debemos disponer nuestro código de aserción en la función then(). Observe que mostramos esta promesa en la prueba y que no tenemos una función catch() que capturar cuando se rechaza la Promise.

      Mostramos la promesa para que cualquier error que aparezca en la función then() se extienda a la función it(). Si los errores no aparecen, Mocha no hará que el caso de prueba fracase. Al probar promesas, deberá usar return en la Promise que se esté probando. Si no es así, tendrá el riesgo de obtener una instancia de falso positivo.

      También omitimos la clausula catch() porque Mocha puede detectar los casos en que se rechaza una promesa. Si se rechaza, automáticamente no superará la prueba.

      Ahora que nuestra prueba está lista, guarde y cierre el archivo y luego ejecute Mocha con npm test, también para confirmar que el resultado sea satisfactorio:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)

      Cambianis nuestro código y la prueba para usar promesas y ahora sabemos con seguridad que funciona. Sin embargo, los patrones asíncronos más recientes utilizan las palabras claves async y await a fin de que no tengamos que crear varias funciones then() para gestionar resultados satisfactorios. Vamos a ver cómo podemos hacer la prueba con async y await.

      async/await

      Las palabras claves async y await hacen que trabajar con promesas sea menos complicado. Una vez que definimos una función como asíncrona con la palabra clave async, podemos obtener cualquier resultado futuro en esa función con la palabra clave await. De esta forma, podemos usar promesas sin tener que usar las funciones then() o catch().

      Podemos simplificar nuestra prueba saveToFile() basada en promesas con async y await. En su editor de texto, realice estas pequeñas ediciones en la prueba saveToFile() en index.test.js:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", async function() {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      El primer cambio es que la función usada por la función it() ahora tiene la palabra clave async cuando se define. Esto nos permite usar la palabra clave await dentro de su cuerpo.

      El segundo cambio se encuentra cuando invocamos saveToFile(). La palabra clave await se usa antes de se invocación. Ahora, Node.js sabe que debe esperar hasta que se resuelva esta función antes de continuar con la prueba.

      Nuestro código de función es más fácil de leer ahora que movimos el código que estaba en la función then() al cuerpo de la función it(). Ejecutar este código con npm test produce este resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)

      Ahora podemos probar las funciones asíncronas usando cualquiera de estos paradigmas asíncronos de forma apropiada.

      Abarcamos un terreno amplio con las pruebas de código síncrono y asíncrono a través de Mocha. A continuación, profundizar en otras funcionalidades que ofrece Mocha para mejorar nuestra experiencia de prueba; en particular, la forma en que los enlaces pueden cambiar entornos de prueba.

      Paso 5: Usar enlaces para mejorar casos de prueba

      Los enlaces son una función útil de Mocha que nos permite configurar el entorno antes y después de una prueba. Normalmente, añadimos hooks en un bloque de funciones describe(), ya que contienen lógica de configuración y desglose específica para algunos casos de prueba.

      Mocha ofrece cuatro enlaces que podemos usar en nuestras pruebas:

      • before: este enlace se ejecuta una vez antes de que se inicie la primera prueba.
      • beforeEach: este se ejecuta antes de cada caso de prueba.
      • after: este se ejecuta una vez después que se completa el último caso de prueba.
      • afterEach: este se ejecuta después de cada caso de prueba.

      Cuando probamos una función o característica varias veces, los enlaces son útiles, ya que nos permiten separar el código de configuración de la prueba (como la creación del objeto todos) desde el código de aserción de esta.

      Para ver el valor de los enlaces, añadiremos más pruebas a nuestro bloque de pruebas saveToFile().

      Aunque confirmamos que podemos guardar nuestros elementos TODO en un archivo, solo guardamos un elemento. Además, el elemento no se marcó como completado. Añadiremos más pruebas para asegurarnos de que los diferentes aspectos de nuestro módulo funcionen.

      Primero, añadiremos una segunda prueba para confirmar que nuestro archivo se guarde correctamente cuando hayamos completado un elemento TODO. Abra el archivo index.test.js en su editor de texto:

      Cambie la última prueba para obtener lo siguiente:

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          it("should save a single TODO", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.complete("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      La prueba es similar a lo que teníamos antes. Las principales diferencias radican en que invocamos la función complete() antes de invocar saveToFile() y que ahora el valor de nuestros expectedFileContents es true en vez de false para el valor de la columna completed.

      Guarde el archivo y ciérrelo.

      Ejecutaremos nuestra nueva prueba y todas las demás con npm test:

      Esto dará el siguiente resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)

      Funciona como se espera. Sin embargo, esto se puede mejorar. Ambas tienen que crear una instancia de un objeto Todos al principio de la prueba. A medida que añadimos más casos de prueba, esto rápidamente se vuelve repetitivo y desperdicia mucha memoria. Además, cada vez que ejecutamos la prueba se crea un archivo. Alguien menos familiarizado con el módulo puede confundir esto con un resultado real. Estaría bien que limpiásemos nuestros archivos resultantes tras la prueba.

      Realizaremos estas mejoras usando enlaces de pruebas. Usaremos el enlace beforeEach() para configurar nuestro artefacto de prueba de elementos TODO. Un artefacto de prueba es cualquier estado uniforme que se usa en una prueba. En nuestro caso, nuestro artefacto de prueba es un nuevo objeto todos que tiene un elemento TODO ya añadido. A continuación usaremos afterEach() para eliminar el archivo creado por la prueba.

      En index.test.js, realice los siguientes cambios a su última prueba para saveToFile():

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          beforeEach(function () {
              this.todos = new Todos();
              this.todos.add("save a CSV");
          });
      
          afterEach(function () {
              if (fs.existsSync("todos.csv")) {
                  fs.unlinkSync("todos.csv");
              }
          });
      
          it("should save a single TODO without error", async function () {
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync("todos.csv"), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              this.todos.complete("save a CSV");
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Desglosaremos todos los cambios que realizamos. Añadimos un bloque beforeEach() al bloque de pruebas:

      todos/index.test.js

      ...
      beforeEach(function () {
          this.todos = new Todos();
          this.todos.add("save a CSV");
      });
      ...
      

      Estas dos líneas de código crean un nuevo objeto Todos que estará disponible en cada una de nuestras pruebas. Con Mocha, el objeto this en beforeEach() hace referencia al mismo objeto this en it(). this es el mismo en todos los bloques de código dentro del bloque describe(). Para obtener más datos sobre this, consulte nuestro tutorial Información sobre This, Bind, Call y Apply en JavaScript.

      Este potente intercambio de contexto es el motivo por el que podemos crear rápidamente artefactos que funcionarán para nuestras dos pruebas.

      A continuación, limpiaremos nuestro archivo CSV en la función afterEach():

      todos/index.test.js

      ...
      afterEach(function () {
          if (fs.existsSync("todos.csv")) {
              fs.unlinkSync("todos.csv");
          }
      });
      ...
      

      Si nuestra prueba falló, es posible que no haya creado un archivo. Por eso verificamos si el archivo existe antes de usar la función unlinkSync() para eliminarlo.

      Los cambios restantes cambian la referencia de todos, creados previamente en la función it(), a this.todos, que está disponible en el contexto de Mocha. También eliminamos las líneas que previamente crearon instancias de todos en los casos de prueba individuales.

      Ahora, ejecutaremos este archivo para confimar que nuestras pruebas aún funcionen. Introduzca npm test en su terminal para obtener lo siguiente:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)

      Los resultados son los mismos y, como beneficio, redujimos ligeramente el tiempo de configuración para las nuevas pruebas de la función saveToFile() y encontramos una solución para el archivo CSV residual.

      Conclusión

      A lo largo de este tutorial , escribió un módulo de Node.js para administrar los elementos TODO y probó el código manualmente usando el REPL de Node.js. Luego, creó un archivo de prueba y usó el marco Mocha para ejecutar pruebas automatizadas. Con el módulo assert, pudo verificar que su código funciona. También probó funciones síncronas y asíncronas con Mocha. Finalmente, creó enlaces con Mocha que aportan legibilidad y una capacidad de mantenimiento muy superiores al escribir varios casos de prueba relacionados.

      Ahora que cuenta con este conocimiento, anímese a escribir pruebas para nuevos módulos de Node.js que cree. ¿Puede pensar en las entradas y los resultados de su función y escribir su prueba antes que su código?

      Si desea más información sobre el marco de prueba Mocha, consulte la documentación oficial de Mocha. Si desea seguir aprendiendo sobre Node.js, puede volver a la página de la serie Cómo desarrollar código en Node.js.



      Source link

      Cómo instalar y usar Radamsa para probar programas y servicios de red con fuzzing en Ubuntu 18.04


      El autor seleccionó la Electronic Frontier Foundation Inc para recibir una donación como parte del programa Write for DOnations.

      Introducción

      Las amenazas de seguridad son cada vez más complejas, por lo que los desarrolladores y los administradores de sistemas deben adoptar un enfoque proactivo para defender y probar la seguridad de sus aplicaciones.

      Un método frecuente para probar la seguridad de aplicaciones o servicios de red de clientes es el fuzzing, que implica enviar datos no válidos o con formato incorrecto a la aplicación en cuestión y analizar su respuesta. Es útil para ayudar a probar cuán resistente y sólida es una aplicación ante entradas inesperadas, como datos corruptos o ataques reales.

      Radamsa es una herramienta de fuzzing de código abierto que puede generar casos de prueba basados en datos de entrada especificados por el usuario. Se puede programar mediante scripts por completo y se utiliza para detectar vulnerabilidades en aplicaciones reales, como Gzip, de forma exitosa.

      A través de este tutorial, instalará y utilizará Radamsa para probar aplicaciones de línea de comandos y de red con fuzzing usando sus propios casos de prueba.

      Advertencia: Radamsa es una herramienta de pruebas de penetración que le permite identificar vulnerabilidades o deficiencias en sistemas o aplicaciones determinados. No debe usar las vulnerabilidades que detecte Radamsa para ninguna forma de comportamiento imprudente, daño o explotación malintencionada. Las vulnerabilidades se deben informar de manera ética a la persona encargada del mantenimiento de la aplicación afectada y no se deben divulgar públicamente sin permiso explícito.

      Requisitos previos

      Para completar esta guía, necesitará lo siguiente:

      Advertencia: Radamsa puede hacer que aplicaciones o sistemas se ejecuten de forma inestable o se bloqueen, por lo que debe ejecutarlo únicamente en un entorno en el que esté preparado para esto, como un servidor dedicado. Asegúrese también de contar con el permiso explícito por escrito del propietario del sistema que probará con fuzzing antes proceder.

      Una vez que tenga todo esto listo, inicie sesión en su servidor como usuario no root.

      Paso 1: Instalar Radamsa

      Primero, descargará y compilará Radamsa para comenzar a utilizarlo en su sistema. El código fuente de Radamsa está disponible en el repositorio oficial de GitLab.

      Comience actualizando el índice de paquetes locales de modo que se refleje cualquier cambio anterior:

      Luego, instalará los paquetes gcc, git, make y wget necesarios para compilar el código fuente en un binario ejecutable:

      • sudo apt install gcc git make wget

      Después de confirmar la instalación, apt descargará e instalará los paquetes específicos y todas sus dependencias necesarias.

      A continuación, descargará una copia del código fuente de Radamsa clonándolo del repositorio alojado en GitLab:

      • git clone https://gitlab.com/akihe/radamsa.git

      Al hacerlo, se creará un directorio llamado radamsa que contendrá el código fuente de la aplicación. Diríjase al directorio para comenzar a compilar el código:

      Luego, puede iniciar el proceso de compilación usando make:

      Por último, puede instalar el binario de Radamsa compilado en su $PATH:

      Una vez completado esto, puede verificar la versión instalada para asegurar que todo funcione:

      El resultado debe tener un aspecto similar al siguiente:

      Output

      Radamsa 0.6

      Si ve el error radamsa: command not found, verifique que todas las dependencias necesarias se hayan instalado y que no se haya habido errores durante la compilación.

      Ahora que instaló Radamsa, puede comenzar a generar algunos casos de prueba de muestra para comprender el funcionamiento de Radamsa y sus aplicaciones.

      Paso 2: Generar casos de prueba con fuzzing

      Ahora que instaló Radamsa, puede usarlo para generar casos de pruebas con fuzzing.

      Un caso de prueba es información que se utilizará como entrada para el programa que someta a prueba. Por ejemplo, si prueba con fuzzing un programa de archivo, como Gzip, un caso de prueba puede ser un archivo de almacenamiento que intente descomprimir.

      Nota: Radamsa manipula los datos de entrada de una gran variedad de formas inesperadas, como la repetición extrema, la inversión de bits y la inserción de caracteres de control, entre otras. Antes de proceder, tenga en cuenta que esto puede hacer que su sesión de terminal se interrumpa o se vuelva inestable.

      Primero, pase un texto sencillo a Radamsa para ver lo que sucede:

      • echo "Hello, world!" | radamsa

      Con esto, se manipularán (o expondrán a fuzzing) los datos ingresados y se generará un caso de prueba, como el siguiente:

      Output

      Hello,, world!

      En este caso, Radamsa agregó una coma adicional entre Hello y world. A pesar de que no parezca un cambio significativo, en algunas aplicaciones, esto puede hacer que los datos se interpreten de forma incorrecta.

      Hagamos un nuevo intento ejecutando el mismo comando. Obtendrá un resultado distinto:

      Output

      Hello, '''''''wor'd!

      Esta vez, se insertaron varias comillas simples (') en la cadena, entre ellas, una que sobrescribió la l en world. Las posibilidades de que este caso de prueba en particular genere problemas en aplicaciones son más altas, dado que se suelen utilizar comillas simples y dobles para separar diferentes porciones de datos de una lista.

      Probemos una vez más:

      Output

      Hello, $+$PATHu0000`xcalc`world!

      En este caso, Radamsa insertó una cadena de inserción de shell, que será útil para realizar pruebas en busca de vulnerabilidades de inserción de comandos en la aplicación que pruebe.

      Usó Radamsa para someter a fuzzing una cadena de entrada y producir una serie de casos de prueba. A continuación, utilizará Radamsa para someter a fuzzing una aplicación de línea de comandos.

      Paso 3: Someter a fuzzing una aplicación de línea de comandos

      En este paso, utilizará Radamsa para aplicar fuzzing a una aplicación de línea de comandos e informar los bloqueos que se produzcan.

      La técnica exacta para hacer pruebas con fuzzing varía en gran medida de un programa a otro, así como la eficacia de cada método. Sin embargo, en este tutorial usaremos jq como ejemplo, que es un programa de línea de comandos para procesar datos de JSON.

      Puede usar cualquier otro programa similar siempre y cuando aplique el principio general de tomar algún tipo de datos estructurados o no estructurados, hacer algo con ellos y luego producir un resultado. Por ejemplo, este caso también funcionaría con Gzip, Grep, bc y tr, entre otros.

      Si aún no instaló jq, puede hacerlo usando apt:

      De esta manera, jq quedará instalado.

      Para comenzar la prueba con fuzzing, cree un archivo JSON de ejemplo que utilizará como entrada para Radamsa:

      Luego, añada los siguientes datos de JSON de ejemplo al archivo:

      test.json

      {
        "test": "test",
        "array": [
          "item1: foo",
          "item2: bar"
        ]
      }
      

      Puede analizar este archivo usando jq si desea verificar que la sintaxis de JSON sea válida:

      Si el JSON es válido, jq mostrará el archivo. De lo contrario, mostrará un error que puede usar para corregir la sintaxis cuando sea necesario.

      A continuación, aplique fuzzing al archivo JSON de prueba usando Radamsa y, luego, páselo a jq. Esto hará que jq lea el caso de prueba sometido a fuzzing o manipulado en lugar de los datos JSON originales válidos.

      Si Radamsa muestra los datos de JSON de una forma que siga siendo válida desde el punto de vista sintáctico, jq mostrará los datos, pero con los cambios que le haya realizado Radamsa.

      Alternativamente, si Radamsa hace que los datos de JSON no sean válidos, jq mostrará un error que corresponda. Por ejemplo:

      Output

      parse error: Expected separator between values at line 5, column 16

      El resultado alternativo sería que jq no pudiera manejar los datos sometidos a fuzzing de forma correcta, lo cual haría que se bloqueara o no funcionara de forma adecuada. Esto es lo que realmente se busca con el método de fuzzing, ya que podría indicar una vulnerabilidad de seguridad, como un desbordamiento de búfer o una inserción de comandos.

      Para realizar pruebas de detección de vulnerabilidades como estas de forma más eficiente, se puede usar una secuencia de comandos de Bash a fin de automatizar el proceso de fuzzing, incluidos los procesos de generar casos de prueba, pasarlos al programa de destino y detectar cualquier resultado pertinente.

      Cree un archivo llamado jq-fuzz.sh:

      El contenido exacto de la secuencia de comandos variará según el tipo de programa que someta a fuzzing y los datos de entrada. Sin embargo, en el caso de jq y otros programas similares, la secuencia de comandos siguiente será suficiente.

      Copie la secuencia de comandos a su archivo jq-fuzz.sh:

      jq-fuzz.sh

      #!/bin/bash
      while true; do
        radamsa test.json > input.txt
        jq . input.txt > /dev/null 2>&1
        if [ $? -gt 127 ]; then
          cp input.txt crash-`date +s%.%N`.txt
          echo "Crash found!"
        fi
      done
      

      Esta secuencia de comandos contiene while para hacer que el contenido se repita reiteradamente. Cada vez que se repita la secuencia de comandos, Radamsa generará un caso de prueba basado en test.json y lo guardará en input.txt.

      Luego, el caso de prueba input.txt se ejecutará en jq y todo el resultado estándar y de error se reenviará a /dev/null para evitar saturar la pantalla del terminal.

      Por último, se verifica el valor de salida de jq. Si el valor de salida es superior a 127, esto indica una interrupción irrecuperable (un bloqueo), por lo que los datos de entrada se guardan para su análisis posterior en un archivo llamado crash- seguido de la fecha actual en segundos y nanosegundos de Unix.

      Marque la secuencia de comandos como ejecutable y ejecútela para comenzar a probar jq con fuzzing de forma automática:

      • chmod +x jq-fuzz.sh
      • ./jq-fuzz.sh

      Puede pulsar CTRL+C en cualquier momento para finalizar la secuencia de comandos. Luego, puede verificar si se encontraron errores utilizando ls para mostrar una lista de directorios en la que se encuentran los archivos de bloqueos que se hayan creado.

      Se recomienda mejorar los datos de JSON de entrada, dado que probablemente el uso de un archivo de entrada más complejo mejore la calidad de los resultados de la aplicación de fuzzing. Evite usar un archivo grande o uno que contenga muchos datos repetidos; lo ideal es usar un archivo de entrada que sea pequeño y, no obstante, tenga la mayor cantidad de elementos “complejos” posible. Por ejemplo, un buen archivo de entrada debe contener muestras de datos almacenados en todos los formatos, incluso cadenas, enteros, booleanos, listas, objetos y datos anidados si es posible.

      Utilizó Radamsa para probar una aplicación de línea de comandos con fuzzing. A continuación, utilizará Radamsa para probar solicitudes a servicios de red.

      Paso 4: Probar solicitudes a servicios de red con fuzzing

      Radamsa también se puede utilizar para probar servicios de red, ya sea como cliente o servidor de red. En este paso, utilizará Radamsa para probar un servicio de red con fuzzing y Radamsa funcionará como cliente.

      El propósito de probar servicios de red con fuzzing es determinar cuán resistente es un servicio de red en particular ante el envío de datos maliciosos o con formato incorrecto. Muchos servicios de red, como los servidores web o DNS, suelen estar expuestos a Internet, lo cual los convierte en un objetivo común para los atacantes. Un servicio de red que no sea suficientemente resistente a la recepción de datos con formato incorrecto puede bloquearse o, lo que sería peor aún, fallar en estado abierto. Esto podría permitir a los atacantes leer datos confidenciales, como claves de cifrado o datos de usuarios.

      Si bien la técnica específica para probar servicios de red con fuzzing varía en gran medida según el servicio de red, en este ejemplo usaremos Radamsa para probar un servidor web básico que proporciona contenido HTML estático.

      Primero, debe configurar el servidor web para su uso en pruebas. Puede hacerlo usando el servidor de desarrollo incorporado que viene con el paquete php-cli. También necesitará curl para probar su servidor web.

      Si no tiene php-cli ni curl instalados, puede instalarlos usando apt:

      • sudo apt install php-cli curl

      A continuación, cree un directorio para almacenar los archivos de su servidor web y posiciónese en él:

      Luego, cree un archivo HTML que contenga texto de ejemplo:

      Añada lo siguiente al archivo:

      index.html

      <h1>Hello, world!</h1>
      

      Ahora, podrá ejecutar su servidor web de PHP. Tendrá que poder ver el registro del servidor web mientras siga utilizando otra sesión de terminal. Por ello, abra otra sesión y aplique SSH al servidor para lo siguiente:

      • cd ~/www
      • php -S localhost:8080

      Con esto, se mostrará algo similar a lo siguiente:

      Output

      PHP 7.2.24-0ubuntu0.18.04.1 Development Server started at Wed Jan 1 16:06:41 2020 Listening on http://localhost:8080 Document root is /home/user/www Press Ctrl-C to quit.

      Ahora, podrá regresar a su sesión de terminal original y verificar que el servidor web esté funcionando mediante curl:

      Con esto, se mostrará el archivo de ejemplo index.html que creó anteriormente:

      Output

      <h1>Hello, world!</h1>

      Solo se debe poder acceder a su servidor web de forma local, por lo que no debe abrir ningún puerto en su firewall para él.

      Ahora que configuró su servidor web de prueba, podrá comenzar a realizar pruebas con fuzzing utilizando Radamsa.

      Primero, deberá crear una solicitud HTTP de ejemplo a fin de usarla como información de entrada para Radamsa. Cree un archivo nuevo para almacenar esto en la siguiente ubicación:

      Luego, copie la siguiente solicitud HTTP de ejemplo al archivo:

      http-request.txt

      GET / HTTP/1.1
      Host: localhost:8080
      User-Agent: test
      Accept: */*
      

      A continuación, puede usar Radamsa para enviar esta solicitud HTTP a su servidor web local. Para hacerlo, deberá usar Radamsa como cliente TCP, lo que se puede hacer especificando una dirección IP y un puerto para establecer conexión:

      • radamsa -o 127.0.0.1:8080 http-request.txt

      Nota: Tenga en cuenta que usar Radamsa como cliente TCP, puede hacer que se transmitan datos maliciosos o con formato incorrecto a través de la red. Esto puede provocar errores; por ello, debe asegurarse de acceder únicamente a las redes que tenga permitido probar o, preferentemente, aténgase a utilizar la dirección de host local (127.0.0.1).

      Por último, si ve los registros de salida de su servidor web local observará que recibió las solicitudes, pero probablemente no las haya procesado porque no eran válidas o tenían un formato incorrecto.

      Los registros de salida se mostrarán en su segunda ventana de terminal:

      Output

      [Wed Jan 1 16:26:49 2020] 127.0.0.1:49334 Invalid request (Unexpected EOF) [Wed Jan 1 16:28:04 2020] 127.0.0.1:49336 Invalid request (Malformed HTTP request) [Wed Jan 1 16:28:05 2020] 127.0.0.1:49338 Invalid request (Malformed HTTP request) [Wed Jan 1 16:28:07 2020] 127.0.0.1:49340 Invalid request (Unexpected EOF) [Wed Jan 1 16:28:08 2020] 127.0.0.1:49342 Invalid request (Malformed HTTP request)

      Para obtener resultados óptimos y asegurarse de que los bloqueos se registren, es conveniente escribir una secuencia de comandos de automatización similar a la que se utilizó en el paso 3. También debe considerar la posibilidad de usar un archivo de entrada más complejo, que puede contener elementos añadidos, como encabezados HTTP adicionales.

      De esta manera, probó un servicio de red con fuzzing usando Radamsa como cliente TCP. A continuación, probará a un cliente de red utilizando Radamsa como servidor.

      Paso 5: Probar aplicaciones clientes de red con fuzzing

      En este paso, utilizará Radamsa para probar una aplicación cliente de red con fuzzing. Esto se logra interceptando respuestas de un servicio de red y sometiéndolas a fuzzing antes de que las reciba el cliente.

      El propósito de este tipo de fuzzing es probar la resistencia de las aplicaciones clientes de red a la recepción de datos maliciosos o de formato incorrecto de servicios de red. Por ejemplo, probará un navegador web (cliente) que recibirá HTML con formato incorrecto de un servidor web (servicio de red) o un cliente DNS que recibirá respuestas DNS con formato incorrecto de un servidor DNS.

      Al igual que en el caso de aplicaciones de línea de comandos o servicios de red, la técnica exacta para probar con fuzzing cada aplicación cliente de red varía considerablemente. Sin embargo, en este ejemplo utilizará whois, una aplicación de envío y recepción simple basada en TCP.

      La aplicación whois se utiliza para enviar solicitudes a servidores WHOIS y recibir registros WHOIS como respuesta. WHOIS funciona a través del puerto TCP 43 con texto no cifrado, por lo que es ideal para realizar pruebas con fuzzing en redes.

      Si aún no cuenta con whois, puede instalarlo usando apt:

      Primero, deberá adquirir una respuesta whois de muestra para usarla como dato de entrada. Puede hacerlo enviando una solicitud whois y guardando el resultado en un archivo. Puede usar cualquier dominio que desee, dado que probará el programa whois de forma local con datos de muestra:

      • whois example.com > whois.txt

      A continuación, deberá configurar Radamsa como un servidor que proporcione versiones distorsionadas de esta respuesta whois. Deberá poder continuar usando su terminal una vez que Radamsa esté en ejecución en el modo de servidor, por lo que se recomienda abrir otra sesión de terminal y conexión SSH con su servidor para lo siguiente:

      • radamsa -o :4343 whois.txt -n inf

      Con esto, Radamsa se ejecutará en el modo de servidor TCP y enviará una versión de whois.txt sometida a fuzzing cada vez que se establezca una conexión con el servidor, independientemente de los datos de solicitud que se reciban.

      Ahora, puede proceder con la prueba de la aplicación cliente whois. Deberá realizar una solicitud whois normal para cualquier dominio que prefiera (no es necesario que sea el mismo al que se aplican los datos de muestra), pero con whois apuntando a su servidor de Radamsa local:

      • whois -h localhost:4343 example.com

      Recibirá como respuesta sus datos de muestra, aunque distorsionados por Radamsa. Puede continuar realizando solicitudes al servidor local mientras Radamsa esté en ejecución. Este enviará una respuesta diferente en cada ocasión.

      Al igual que con los servicios de red, para mejorar la eficacia de estas pruebas del cliente de red con fuzzing y asegurarse de que se registren los bloqueos, es conveniente escribir una secuencia de comandos de automatización similar a la que se utilizó en el paso 3.

      En este paso final, utilizó Radamsa para probar una aplicación cliente de red con fuzzing.

      Conclusión

      A través de este artículo, configuró Radamsa y lo utilizó para probar una aplicación de línea de comandos, un servicio de red y un cliente de red. Ahora tiene los conocimientos básicos necesarios para probar sus propias aplicaciones con fuzzing y mejorar su solidez y resistencia contra ataques.

      Si desea explorar Radamsa en mayor profundidad, puede revisar el archivo README de Radamsa de forma detallada; contiene más información técnica y ejemplos relacionados con los usos posibles de la herramienta:

      También puede probar otras herramientas de fuzzing como American Fuzzy Lop (AFL), una herramienta de fuzzing avanzada diseñada para probar aplicaciones binarias con una velocidad y una precisión sumamente altas:



      Source link