b Porbando el backendc Administración de usuariosd Autenticación de token

    a

    Estructura de la aplicación backend, introducción a las pruebas

    Continuemos nuestro trabajo en el backend del aplicación de notas que comenzamos en parte 3.

    Estructura del proyecto

    Antes de pasar al tema de las pruebas, modificaremos la estructura de nuestro proyecto para cumplir con las mejores prácticas de Node.js.

    Después de realizar los cambios en la estructura de directorios de nuestro proyecto, terminamos con la siguiente estructura:

    ├── index.js
    ├── app.js
    ├── build
    │   └── ...
    ├── controllers
    │   └── notes.js
    ├── models
    │   └── note.js
    ├── package-lock.json
    ├── package.json
    ├── utils
    │   ├── config.js
    │   ├── logger.js
    │   └── middleware.js  

    Hasta ahora hemos estado usando console.log y console.error para imprimir información diferente del código. Sin embargo, esta no es una buena forma de hacer las cosas. Separemos todas las impresiones a la consola en su propio módulo utils/logger.js:

    const info = (...params) => {
      console.log(...params)
    }
    
    const error = (...params) => {
      console.error(...params)
    }
    
    module.exports = {
      info, error
    }

    El logger tiene dos funciones, info para imprimir mensajes de registro normales y error para todos los mensajes de error.

    Extraer registros en su propio módulo es una buena idea en más de un sentido. Si quisiéramos comenzar a escribir registros en un archivo o enviarlos a un servicio de registro externo como graylog o papertrail solo tendríamos que hacer cambios en un solo lugar.

    El contenido del archivo index.js utilizado para iniciar la aplicación se simplifica de la siguiente manera:

    const app = require('./app') // la aplicación Express real
    const http = require('http')
    const config = require('./utils/config')
    const logger = require('./utils/logger')
    
    const server = http.createServer(app)
    
    server.listen(config.PORT, () => {
      logger.info(`Server running on port ${config.PORT}`)
    })

    El archivo index.js solo importa la aplicación real desde el archivo app.js y luego inicia la aplicación. La función info del módulo de registro se utiliza para la impresión de la consola que indica que la aplicación se está ejecutando.

    El manejo de las variables de entorno se extrae en un archivo utils/config.js separado:

    require('dotenv').config()
    
    const PORT = process.env.PORT
    const MONGODB_URI = process.env.MONGODB_URI
    
    module.exports = {
      MONGODB_URI,
      PORT
    }

    Las otras partes de la aplicación pueden acceder a las variables de entorno importando el módulo de configuración:

    const config = require('./utils/config')
    
    logger.info(`Server running on port ${config.PORT}`)

    Los controladores de ruta también se han movido a un módulo dedicado. Los controladores de eventos de las rutas se conocen comúnmente como controladores, y por esta razón hemos creado un nuevo directorio de controllers. Todas las rutas relacionadas con las notas están ahora en el módulo notes.js bajo el directorio controllers.

    El contenido del módulo notes.js es el siguiente:

    const notesRouter = require('express').Router()
    const Note = require('../models/note')
    
    notesRouter.get('/', (request, response) => {
      Note.find({}).then(notes => {
        response.json(notes)
      })
    })
    
    notesRouter.get('/:id', (request, response, next) => {
      Note.findById(request.params.id)
        .then(note => {
          if (note) {
            response.json(note)
          } else {
            response.status(404).end()
          }
        })
        .catch(error => next(error))
    })
    
    notesRouter.post('/', (request, response, next) => {
      const body = request.body
    
      const note = new Note({
        content: body.content,
        important: body.important || false,
        date: new Date()
      })
    
      note.save()
        .then(savedNote => {
          response.json(savedNote)
        })
        .catch(error => next(error))
    })
    
    notesRouter.delete('/:id', (request, response, next) => {
      Note.findByIdAndRemove(request.params.id)
        .then(() => {
          response.status(204).end()
        })
        .catch(error => next(error))
    })
    
    notesRouter.put('/:id', (request, response, next) => {
      const body = request.body
    
      const note = {
        content: body.content,
        important: body.important,
      }
    
      Note.findByIdAndUpdate(request.params.id, note, { new: true })
        .then(updatedNote => {
          response.json(updatedNote)
        })
        .catch(error => next(error))
    })
    
    module.exports = notesRouter

    Esto es casi una copia y pegado exacta de nuestro archivo index.js anterior.

    Sin embargo, hay algunos cambios importantes. Al principio del archivo, creamos un nuevo objeto router:

    const notesRouter = require('express').Router()
    
    //...
    
    module.exports = notesRouter

    El módulo exporta el enrutador para que esté disponible para todos los consumidores del módulo.

    Todas las rutas están ahora definidas para el objeto enrutador, de manera similar a lo que habíamos hecho anteriormente con el objeto que representa la aplicación completa.

    Vale la pena señalar que los caminos en los controladores de ruta se han acortado. En la versión anterior, teníamos:

    app.delete('/api/notes/:id', (request, response) => {

    Y en la versión actual, tenemos:

    notesRouter.delete('/:id', (request, response) => {

    Entonces , ¿qué son exactamente estos objetos de enrutador? El manual Express proporciona la siguiente explicación:

    Un objeto de enrutador es un instancia aislada de middleware y rutas. Puede pensar en ella como una "mini-aplicación", capaz solo de realizar funciones de middleware y enrutamiento. Cada aplicación Express tiene un enrutador de aplicación incorporado.

    El enrutador es de hecho un middleware, que se puede utilizar para definir "rutas relacionadas" en un solo lugar, que normalmente se coloca en su propio módulo.

    El archivo app.js que crea la aplicación real , toma el enrutador como se muestra a continuación:

    const notesRouter = require('./controllers/notes')
    app.use('/api/notes', notesRouter)

    El enrutador que definimos anteriormente se usa si la URL de la solicitud comienza con /api/notes. Por esta razón, el objeto notesRouter solo debe definir las partes relativas de las rutas, es decir, la ruta vacía / o solo el parámetro /:id.

    Después de realizar estos cambios, nuestro archivo app.js se ve así:

    const config = require('./utils/config')
    const express = require('express')
    const app = express()
    const cors = require('cors')
    const notesRouter = require('./controllers/notes')
    const middleware = require('./utils/middleware')
    const logger = require('./utils/logger')
    const mongoose = require('mongoose')
    
    logger.info('connecting to', config.MONGODB_URI)
    
    mongoose.connect(config.MONGODB_URI)
      .then(() => {
        logger.info('connected to MongoDB')
      })
      .catch((error) => {
        logger.error('error connecting to MongoDB:', error.message)
      })
    
    app.use(cors())
    app.use(express.static('build'))
    app.use(express.json())
    app.use(middleware.requestLogger)
    
    app.use('/api/notes', notesRouter)
    
    app.use(middleware.unknownEndpoint)
    app.use(middleware.errorHandler)
    
    module.exports = app

    El archivo utiliza un middleware diferente, y uno de ellos es el notesRouter que se adjunta a la ruta /api/notes.

    Nuestro middleware personalizado se ha movido a un nuevo módulo utils/middleware.js:

    const logger = require('./logger')
    
    const requestLogger = (request, response, next) => {
      logger.info('Method:', request.method)
      logger.info('Path:  ', request.path)
      logger.info('Body:  ', request.body)
      logger.info('---')
      next()
    }
    
    const unknownEndpoint = (request, response) => {
      response.status(404).send({ error: 'unknown endpoint' })
    }
    
    const errorHandler = (error, request, response, next) => {
      logger.error(error.message)
    
      if (error.name === 'CastError') {
        return response.status(400).send({ error: 'malformatted id' })
      } else if (error.name === 'ValidationError') {
        return response.status(400).json({ error: error.message })
      }
    
      next(error)
    }
    
    module.exports = {
      requestLogger,
      unknownEndpoint,
      errorHandler
    }

    La responsabilidad de establecer la conexión con la base de datos se ha entregado al módulo app.js. El archivo note.js del directorio models solo define el esquema de Mongoose para las notas.

    const mongoose = require('mongoose')
    
    const noteSchema = new mongoose.Schema({
      content: {
        type: String,
        required: true,
        minlength: 5
      },
      date: {
        type: Date,
        required: true,
      },
      important: Boolean,
    })
    
    noteSchema.set('toJSON', {
      transform: (document, returnedObject) => {
        returnedObject.id = returnedObject._id.toString()
        delete returnedObject._id
        delete returnedObject.__v
      }
    })
    
    module.exports = mongoose.model('Note', noteSchema)

    Para recapitular, la estructura del directorio se ve así después de que se hayan realizado los cambios:

    ├── index.js
    ├── app.js
    ├── build
    │   └── ...
    ├── controllers
    │   └── notes.js
    ├── models
    │   └── note.js
    ├── package-lock.json
    ├── package.json
    ├── utils
    │   ├── config.js
    │   ├── logger.js
    │   └── middleware.js  

    Para aplicaciones más pequeñas, la estructura no importa mucho. Una vez que la aplicación comienza a crecer en tamaño, tendrá que establecer algún tipo de estructura y separar las diferentes responsabilidades de la aplicación en módulos separados. Esto facilitará mucho el desarrollo de la aplicación.

    No existe una estructura de directorio estricta o una convención de nomenclatura de archivos que se requiera para las aplicaciones Express. Para contrastar esto, Ruby on Rails requiere una estructura específica. Nuestra estructura actual simplemente sigue algunas de las mejores prácticas que puede encontrar en Internet.

    Puede encontrar el código para nuestra aplicación actual en su totalidad en la rama part4-1 de este repositorio de Github.

    Si clona el proyecto usted mismo, ejecute el comando npm install antes de iniciar la aplicación con npm start.

    Testing de aplicaciones Node

    Hemos descuidado por completo un área esencial del desarrollo de software, y es la prueba automatizada.

    Comencemos nuestro viaje de prueba mirando las pruebas unitarias. La lógica de nuestra aplicación es tan simple, que no hay mucho que tenga sentido para probar con pruebas unitarias. Creemos un nuevo archivo utils/for_testing.js y escribamos un par de funciones simples que podamos usar para la práctica de escritura de prueba:

    const palindrome = (string) => {
      return string
        .split('')
        .reverse()
        .join('')
    }
    
    const average = (array) => {
      const reducer = (sum, item) => {
        return sum + item
      }
    
      return array.reduce(reducer, 0) / array.length
    }
    
    module.exports = {
      palindrome,
      average,
    }

    La función average usa el método array reduce. Si el método aún no le resulta familiar, ahora es un buen momento para ver los primeros tres videos de la serie Functional Javascript en Youtube.

    Hay muchas bibliotecas de prueba diferentes o ejecutores de prueba disponibles para JavaScript. En este curso utilizaremos una biblioteca de prueba desarrollada y utilizada internamente por Facebook llamada jest, que se asemeja al rey anterior de las bibliotecas de prueba de JavaScript Mocha. Existen otras alternativas, como ava que ha ganado popularidad en algunos círculos.

    Jest es una opción natural para este curso, ya que funciona bien para probar backends y brilla cuando se trata de probar aplicaciones React.

    Usuarios de Windows: Jest puede no funcionar si la ruta del directorio del proyecto contiene un directorio que tiene espacios en su nombre.

    Dado que las pruebas solo se ejecutan durante el desarrollo de nuestra aplicación, instalaremos jest como una dependencia de desarrollo con el comando:

    npm install --save-dev jest

    Definamos el npm script test para ejecutar pruebas con Jest y para informar sobre la ejecución de la prueba con el estilo detallado:

    {
      //...
      "scripts": {
        "start": "node index.js",
        "dev": "nodemon index.js",
        "build:ui": "rm -rf build && cd ../../../2/luento/notes && npm run build && cp -r build ../../../3/luento/notes-backend",
        "deploy": "git push heroku master",
        "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && git push && npm run deploy",
        "logs:prod": "heroku logs --tail",
        "lint": "eslint .",
        "test": "jest --verbose"  },
      //...
    }

    Jest requiere que uno especifique que el entorno de ejecución es Node. Esto se puede hacer agregando lo siguiente al final de package.json:

    {
     //...
     "jest": {
       "testEnvironment": "node"
     }
    }

    Alternativamente, Jest puede buscar un archivo de configuración con el nombre predeterminado jest.config.js, donde podemos definir el entorno de ejecución así:

    module.exports = {
      testEnvironment: 'node',
    };

    Creemos un directorio separado para nuestras pruebas llamado tests y creemos un nuevo archivo llamado palindrome.test.js con el siguiente contenido:

    const palindrome = require('../utils/for_testing').palindrome
    
    test('palindrome of a', () => {
      const result = palindrome('a')
    
      expect(result).toBe('a')
    })
    
    test('palindrome of react', () => {
      const result = palindrome('react')
    
      expect(result).toBe('tcaer')
    })
    
    test('palindrome of releveler', () => {
      const result = palindrome('releveler')
    
      expect(result).toBe('releveler')
    })

    La configuración de ESLint que agregamos al proyecto en la parte anterior se queja de los comandos test y expect en nuestro archivo de prueba, ya que la configuración no permite globals. Eliminemos las quejas agregando "jest": true a la propiedad env en el archivo .eslintrc.js.

    module.exports = {
      "env": {
        "commonjs": true 
        "es6": true,
        "node": true,
        "jest": true,  },
      "extends": "eslint:recommended",
      "rules": {
        // ...
      },
    };

    En la primera fila, el archivo de prueba importa la función a ser probada y la asigna a una variable llamada palindrome:

    const palindrome = require('../utils/for_testing').palindrome

    Los casos de prueba individual se definen con la función test. El primer parámetro de la función es la descripción de la prueba como una cadena. El segundo parámetro es una función, que define la funcionalidad para el caso de prueba. La funcionalidad para el segundo caso de prueba se ve así:

    () => {
      const result = palindrome('react')
    
      expect(result).toBe('tcaer')
    }

    Primero ejecutamos el código a probar, lo que significa que generamos un palíndromo para la cadena react. A continuación, verificamos los resultados con la función expect. Expect envuelve el valor resultante en un objeto que ofrece una colección de funciones matcher, que pueden usarse para verificar la exactitud del resultado. Dado que en este caso de prueba estamos comparando dos cadenas, podemos usar el comparador toBe.

    Como se esperaba, todas las pruebas pasan:

    fullstack content

    Jest espera por defecto que los nombres de los archivos de prueba contengan .test. En este curso, seguiremos la convención de nombrar nuestros archivos de prueba con la extensión .test.js.

    Jest tiene excelentes mensajes de error, rompamos la prueba para demostrar esto:

    test('palindrom of react', () => {
      const result = palindrome('react')
    
      expect(result).toBe('tkaer')
    })

    Ejecutar las pruebas anteriores da como resultado el siguiente mensaje de error:

    fullstack content

    Agreguemos algunas pruebas para la función average, en un nuevo archivo tests/average.test.js.

    const average = require('../utils/for_testing').average
    
    describe('average', () => {
      test('of one value is the value itself', () => {
        expect(average([1])).toBe(1)
      })
    
      test('of many is calculated right', () => {
        expect(average([1, 2, 3, 4, 5, 6])).toBe(3.5)
      })
    
      test('of empty array is zero', () => {
        expect(average([])).toBe(0)
      })
    })

    La prueba revela que la función no funciona correctamente con una matriz vacía (esto se debe a que en JavaScript dividir por cero da como resultado NaN)

    fullstack content

    Arreglar la función es bastante fácil:

    const average = array => {
      const reducer = (sum, item) => {
        return sum + item
      }
    
      return array.length === 0
        ? 0
        : array.reduce(reducer, 0) / array.length
    }

    Si la longitud de la matriz es 0, devolvemos 0, y en todos los demás casos usamos el método reduce para calcular el promedio.

    Hay algunas cosas a tener en cuenta sobre las pruebas que acabamos de escribir. Definimos un bloque describe alrededor de las pruebas al que se le dio el nombre average:

    describe('average', () => {
      // tests
    })

    Se pueden usar bloques de descripción para agrupar pruebas en colecciones lógicas. La salida de prueba de Jest también usa el nombre del bloque describe:

    fullstack content

    Como veremos más adelante, los bloques describe son necesarios cuando queremos ejecutar algunas operaciones de instalación o desmontaje compartidas para un grupo de pruebas.

    Otra cosa a tener en cuenta es que escribimos las pruebas de una manera bastante compacta, sin asignar la salida de la función que se está probando a una variable:

    test('of empty array is zero', () => {
      expect(average([])).toBe(0)
    })