b Testing the backendc User administrationd Token authentication

    a

    Structure of backend application, introduction to testing

    Let's continue our work on the backend of the notes application we started in part 3.

    Project structure

    Before we move into the topic of testing, we will modify the structure of our project to adhere to Node.js best practices.

    After making the changes to the directory structure of our project, we end up with the following structure:

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

    The contents of the index.js file used for starting the application gets simplified as follows:

    const app = require('./app') // the actual Express app
    const http = require('http')
    const config = require('./utils/config')
    
    const server = http.createServer(app)
    
    server.listen(config.PORT, () => {
      console.log(`Server running on port ${config.PORT}`)
    })

    The index.js file only imports the actual application from the app.js file and then starts the application.

    The handling of environment variables is extracted into a separate utils/config.js file:

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

    The other parts of the application can access the environment variables by importing the configuration module:

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

    The route handlers have also been moved into a dedicated module. The event handlers of routes are commonly referred to as controllers, and for this reason we have created a new controllers directory. All of the routes related to notes are now in the notes.js module under the controllers directory.

    The contents of the notes.js module are the following:

    const notesRouter = require('express').Router()
    const Note = require('../models/note')
    
    notesRouter.get('/', (request, response) => {
      Note.find({}).then(notes => {
        response.json(notes.map(note => note.toJSON()))
      })
    })
    
    notesRouter.get('/:id', (request, response, next) => {
      Note.findById(request.params.id)
        .then(note => {
          if (note) {
            response.json(note.toJSON())
          } 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.toJSON())
        })
        .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.toJSON())
        })
        .catch(error => next(error))
    })
    
    module.exports = notesRouter

    This is almost an exact copy-paste of our previous index.js file.

    However, there are a few significant changes. At the very beginning of the file we create a new router object:

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

    The module exports the router to be available for all consumers of the module.

    All routes are now defined for the router object, in a similar fashion to what we had previously done with the object representing the entire application.

    It's worth noting that the paths in the route handlers have shortened. In the previous version, we had:

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

    And in the current version, we have:

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

    So what are these router objects exactly? The Express manual provides the following explanation:

    A router object is an isolated instance of middleware and routes. You can think of it as a “mini-application,” capable only of performing middleware and routing functions. Every Express application has a built-in app router.

    The router is in fact a middleware, that can be used for defining "related routes" in a single place, that is typically placed in its own module.

    The app.js file that creates the actual application, takes the router into use as shown below:

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

    The router we defined earlier is used if the URL of the request starts with /api/notes. For this reason, the notesRouter object must only define the relative parts of the routes, i.e. the empty path / or just the parameter /:id.

    After making these changes, our app.js file looks like this:

    const config = require('./utils/config')
    const express = require('express')
    const bodyParser = require('body-parser')
    const app = express()
    const cors = require('cors')
    const notesRouter = require('./controllers/notes')
    const middleware = require('./utils/middleware')
    const mongoose = require('mongoose')
    
    console.log('connecting to', config.MONGODB_URI)
    
    mongoose.connect(config.MONGODB_URI, { useNewUrlParser: true })
      .then(() => {
        console.log('connected to MongoDB')
      })
      .catch((error) => {
        console.log('error connection to MongoDB:', error.message)
      })
    
    app.use(cors())
    app.use(express.static('build'))
    app.use(bodyParser.json())
    app.use(middleware.requestLogger)
    
    app.use('/api/notes', notesRouter)
    
    app.use(middleware.unknownEndpoint)
    app.use(middleware.errorHandler)
    
    module.exports = app

    The file takes different middleware into use, and one of these is the notesRouter that is attached to the /api/notes route.

    Our custom middleware has been moved to a new utils/middleware.js module:

    const requestLogger = (request, response, next) => {
      console.log('Method:', request.method)
      console.log('Path:  ', request.path)
      console.log('Body:  ', request.body)
      console.log('---')
      next()
    }
    
    const unknownEndpoint = (request, response) => {
      response.status(404).send({ error: 'unknown endpoint' })
    }
    
    const errorHandler = (error, request, response, next) => {
      console.error(error.message)
    
      if (error.name === 'CastError' && error.kind === 'ObjectId') {
        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
    }

    The responsibility of establishing the connection to the database has been given to the app.js module. The note.js file under the models directory only defines the Mongoose schema for notes.

    const mongoose = require('mongoose')
    
    const noteSchema = new mongoose.Schema({
      content: {
        type: String,
        required: true,
        minlength: 5
      },
      date: Date,
      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)

    To recap, the directory structure looks like this after the changes have been made:

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

    For smaller applications the structure does not matter that much. Once the application starts to grow in size, you are going to have to establish some kind of structure, and separate the different responsibilities of the application into separate modules. This will make developing the application much easier.

    There is no strict directory structure or file naming convention that is required for Express applications. To contrast this, Ruby on Rails does require a specific structure. Our current structure simply follows some of the best practices you can come across on the internet.

    You can find the code for our current application in its entirety in the part4-1 branch of this Github repository.

    If you clone the project for yourself, run the npm install command before starting the application with npm start.

    Testing Node applications

    We have completely neglected one essential area of software development, and that is automated testing.

    Let's start our testing journey by looking at unit tests. The logic of our application is so simple, that there is not much that makes sense to test with unit tests. Let's create a new file utils/for_testing.js and write a couple of simple functions that we can use for test writing practice:

    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,
    }

    The average function uses the array reduce method. If the method is not familiar to you yet, then now is a good time to watch the the first three videos from the Functional Javascript series on Youtube.

    There are many different testing libraries or test runners available for JavaScript. In this course we will be using a testing library developed and used internally by Facebook called jest, that resembles the previous king of JavaScript testing libraries Mocha. Other alternatives do exist, like ava that has gained popularity in some circles.

    Jest is a natural choice for this course, as it works well for testing backends, and it shines when it comes to testing React applications.

    Windows users: Jest may not work if the path of the project directory contains a directory that has spaces in its name.

    Since tests are only executed during the development of our application, we will install jest as a development dependency with the command:

    npm install --save-dev jest

    Let's define the npm script test to execute tests with Jest and to report about the test execution with the verbose style:

    {
      //...
      "scripts": {
        "start": "node index.js",
        "watch": "nodemon index.js",
        "build:ui": "rm -rf build && cd ../../osa2/notes/ && npm run build --prod && cp -r build ../../osa3/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"  },
      //...
    }

    In newer versions of Jest, there appears to be the need to specify that the execution environment is Node. This can be done by adding the following to the end of package.json:

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

    Alternatively, Jest can look for a configuration file with the default name jest.config.js, where we can define the execution environment like this:

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

    Let's create a separate directory for our tests called tests and create a new file called palindrome.test.js with the following contents:

    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')
    })

    The ESLint configuration we added to the project in the previous part complains about the test and expect commands in our test file, since the configuration does not allow globals. Let's get rid of the complaints by adding "jest": true to the env property in the .eslintrc.js file.

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

    In the first row, the test file imports the function to be tested and assigns it to a variable called palindrome:

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

    Individual test cases are defined with the test function. The first parameter of the function is the test description as a string. The second parameter is a function, that defines the functionality for the test case. The functionality for the second test case looks like this:

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

    First we execute the code to be tested, meaning that we generate a palindrome for the string react. Next we verify the results with the expect function. Expect wraps the resulting value into an object that offers a collection of matcher functions, that can be used for verifying the correctness of the result. Since in this test case we are comparing two strings, we can use the toBe matcher.

    As expected, all of the tests pass:

    fullstack content

    Jest expects by default that the names of test files contain .test. In this course, we will follow the convention of naming our tests files with the extension .test.js.

    Jest has excellent error messages, let's break the test to demonstrate this:

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

    Running the tests above results in the following error message:

    fullstack content

    Let's add a few tests for the average function, into a new file 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)
      })
    })

    The test reveals that the function does not work correctly with an empty array (this is because in JavaScript dividing by zero results in NaN):

    fullstack content

    Fixing the function is quite easy:

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

    If the length of the array is 0 then we return 0, and in all other cases we use the reduce method to calculate the average.

    There's a few things to notice about the tests that we just wrote. We defined a describe block around the tests that was given the name average:

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

    Describe blocks can be used for grouping tests into logical collections. The test output of Jest also uses the name of the describe block:

    fullstack content

    As we will see later on describe blocks are necessary when we want to run some shared setup or teardown operations for a group of tests.

    Another thing to notice is that we wrote the tests in quite a compact way, without assigning the output of the function being tested to a variable:

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