One place for hosting & domains

      Test a Node RESTful API with Mocha and Chai


      Introduction

      I still remember the satisfaction of being finally able to write the backend part of a bigger app in node and I am sure many of you do it too.

      And then? We need to make sure our app behaves the way we expect and one of the strongly suggested methodologies is software testing. Software testing is crazily useful whenever a new feature is added to the system: Having the test environment already set up which can be run with a single command helps to figure out whether a new feature introduces new bugs.

      In the past, we’ve worked on building a RESTful Node API and authenticating a Node API.

      In this tutorial we are going to write a simple RESTful API with Node.js and use Mocha and Chai to write tests against it. We will test CRUD operations on a bookstore.

      As usual you can build the app step-by-step throughout the tutorial or directly get it on github.

      Mocha: Testing Environment

      Mocha is a javascript framework for Node.js which allows Asynchronous testing. Let’s say it provides the environment in which we can use our favorite assertion libraries to test the code.

      mocha-homepage.

      Mocha comes with tons of great features, the website shows a long list but here are the ones I like the most:

      • simple async support, including promises.
      • async test timeout support.
      • before, after, before each, after each hooks (very useful to clean the environment where each test!).
      • use any assertion library you want, Chai in our tutorial.

      Chai: Assertion Library

      So with Mocha we actually have the environment for making our tests but how do we do test HTTP calls for example? Moreover, How do we test whether a GET request is actually returning the JSON file we are expective, given a defined input? We need an assertion library, that’s why mocha is not enough.

      So here it is Chai, the assertion library for the current tutorial:

      chai homepage

      Chai shines on the freedom of choosing the interface we prefer: “should”, “expect”, “assert” they are all available. I personally use should but you are free to check it out the API and switch to the others two. Lastly Chai HTTP addon allows Chai library to easily use assertions on HTTP requests which suits our needs.

      Prerequisites

      • Node.js: a basic understanding of node.js and is recommended as i wont go too much into detail on building a RESTful API.
      • POSTMAN for making fast HTTP requests to the API.
      • ES6 syntax: I decided to use the latest version of Node (6.*.*) which has the highest integration of ES6 features for better code readibility. If you are not familiar with ES6 you can take a look at the great scotch articles (Pt.1 , Pt.2 and Pt.3) about it but do not worry I am going to spend a few words whenever we encount some “exotic” syntax or declaration.

      Time to setup our Bookstore!

      Project setup

      Directory Structure

      Here is the project directory for our API, something you must have seen before:

      -- controllers 
      ---- models
      ------ book.js
      ---- routes
      ------ book.js
      -- config
      ---- default.json
      ---- dev.json
      ---- test.json
      -- test
      ---- book.js
      package.json
      server.json
      

      Notice the /config folder containing 3 JSON files: As the name suggests, they contain particular configurations for a specific purpose.

      In this tutorial we are going to switch between two databases, one for development and one for testing purposes, thus the files contain the mongodb URI in JSON format:

      dev.json AND default.json

      { "DBHost": "YOUR_DB_URI" }
      

      test.json

      { "DBHost": "YOUR_TEST_DB_URI" }
      

      NB: default.json is optional however let me highlight that files in the config directory are loaded starting from it. For more information about the configuration files (config directory, file order, file format etc.) check out this link.

      Finally, notice /test/book.js, that’s where we are going to write our tests!

      Package.json

      Create the package.json file and paste the following code:

      {
        "name": "bookstore",
        "version": "1.0.0",
        "description": "A bookstore API",
        "main": "server.js",
        "author": "Sam",
        "license": "ISC",
        "dependencies": {
          "body-parser": "^1.15.1",
          "config": "^1.20.1",
          "express": "^4.13.4",
          "mongoose": "^4.4.15",
          "morgan": "^1.7.0"
        },
        "devDependencies": {
          "chai": "^3.5.0",
          "chai-http": "^2.0.1",
          "mocha": "^2.4.5"
        },
        "scripts": {
          "start": "SET NODE_ENV=dev && node server.js",
          "test": "mocha --timeout 10000"
        }
      }
      

      Again the configuration should not surprise anyone who wrote more than a server with node.js, the test-related packages mocha, chai, chai-http are saved in the dev-dependencies (flag --save-dev from command line) while the scripts property allows for two different ways of running the server.

      To run mocha I added the flag --timeout 10000 because I fetch data from a database hosted on mongolab so the default 2 seconds may not be enough.

      Congrats! You made it through the boring part of the tutorial, now it is time to write the server and test it.

      The server

      Main

      Let’s create the file server.js in the root of the project and paste the following code:

      
      let express = require('express');
      let app = express();
      let mongoose = require('mongoose');
      let morgan = require('morgan');
      let bodyParser = require('body-parser');
      let port = 8080;
      let book = require('./app/routes/book');
      let config = require('config'); //we load the db location from the JSON files
      //db options
      let options = { 
                      server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, 
                      replset: { socketOptions: { keepAlive: 1, connectTimeoutMS : 30000 } } 
                    }; 
      
      //db connection      
      mongoose.connect(config.DBHost, options);
      let db = mongoose.connection;
      db.on('error', console.error.bind(console, 'connection error:'));
      
      //don't show the log when it is test
      if(config.util.getEnv('NODE_ENV') !== 'test') {
          //use morgan to log at command line
          app.use(morgan('combined')); //'combined' outputs the Apache style LOGs
      }
      
      //parse application/json and look for raw text                                        
      app.use(bodyParser.json());                                     
      app.use(bodyParser.urlencoded({extended: true}));               
      app.use(bodyParser.text());                                    
      app.use(bodyParser.json({ type: 'application/json'}));  
      
      app.get("/", (req, res) => res.json({message: "Welcome to our Bookstore!"}));
      
      app.route("/book")
          .get(book.getBooks)
          .post(book.postBook);
      app.route("/book/:id")
          .get(book.getBook)
          .delete(book.deleteBook)
          .put(book.updateBook);
      
      
      app.listen(port);
      console.log("Listening on port " + port);
      
      module.exports = app; // for testing
      

      Here are the key concepts:

      • We require the module config to access the configuration file named as the NODE_ENV content to get the mongo db URI parameter for the db connection. This helps us to keep the “real” database clean by testing on another database hidden to our app future users.
      • The enviroment variable NODE_ENV is test against test to disable morgan log in the command line or it would interfere with the test output.
      • The last line of code exports the server for testing purposes.
      • Notice the variables definition using let which makes the variable enclosed to the nearest enclosing block or global if outside any block.

      The remaining lines of codes are nothing new, we simply go through requiring all the necessary modules, define the header options for the communication with the server, craete the specific roots and eventually let the server listen on a defined port.

      Model and Routes

      Time for our book model! Create a file in /app/model/ called book.js and paste the following code:

      let mongoose = require('mongoose');
      let Schema = mongoose.Schema;
      
      //book schema definition
      let BookSchema = new Schema(
        {
          title: { type: String, required: true },
          author: { type: String, required: true },
          year: { type: Number, required: true },
          pages: { type: Number, required: true, min: 1 },
          createdAt: { type: Date, default: Date.now },    
        }, 
        { 
          versionKey: false
        }
      );
      
      // Sets the createdAt parameter equal to the current time
      BookSchema.pre('save', next => {
        now = new Date();
        if(!this.createdAt) {
          this.createdAt = now;
        }
        next();
      });
      
      //Exports the BookSchema for use elsewhere.
      module.exports = mongoose.model('book', BookSchema);
      

      Our book schema has a title, author, the number of pages, the publication year and the date of creation in the db. I set the versionKey to false since it’s useless for the purpose of the tutorial.

      NB: the exotic callback syntax in the .pre() function is an arrow function, a function who has a shorter syntax which, according to the definiton on MDN , “lexically binds the this value (does not bind its own this, arguments, super, or new.target). Arrow functions are always anonymous”.

      Well, pretty much all we need to know about the model so let’s move to the routes.

      in /app/routes/ create a file called book.js and paste the following code:

      let mongoose = require('mongoose');
      let Book = require('../models/book');
      
      /*
       * GET /book route to retrieve all the books.
       */
      function getBooks(req, res) {
          //Query the DB and if no errors, send all the books
          let query = Book.find({});
          query.exec((err, books) => {
              if(err) res.send(err);
              //If no errors, send them back to the client
              res.json(books);
          });
      }
      
      /*
       * POST /book to save a new book.
       */
      function postBook(req, res) {
          //Creates a new book
          var newBook = new Book(req.body);
          //Save it into the DB.
          newBook.save((err,book) => {
              if(err) {
                  res.send(err);
              }
              else { //If no errors, send it back to the client
                  res.json({message: "Book successfully added!", book });
              }
          });
      }
      
      /*
       * GET /book/:id route to retrieve a book given its id.
       */
      function getBook(req, res) {
          Book.findById(req.params.id, (err, book) => {
              if(err) res.send(err);
              //If no errors, send it back to the client
              res.json(book);
          });        
      }
      
      /*
       * DELETE /book/:id to delete a book given its id.
       */
      function deleteBook(req, res) {
          Book.remove({_id : req.params.id}, (err, result) => {
              res.json({ message: "Book successfully deleted!", result });
          });
      }
      
      /*
       * PUT /book/:id to updatea a book given its id
       */
      function updateBook(req, res) {
          Book.findById({_id: req.params.id}, (err, book) => {
              if(err) res.send(err);
              Object.assign(book, req.body).save((err, book) => {
                  if(err) res.send(err);
                  res.json({ message: 'Book updated!', book });
              });    
          });
      }
      
      //export all the functions
      module.exports = { getBooks, postBook, getBook, deleteBook, updateBook };
      

      Here the key concepts:

      • The routes are no more than standard routes, GET, POST, DELETE, PUT to perform CRUD operations on our data.
      • In the function updatedBook() we use Object.assign, a new function introduced in ES6 which, in this case, overrides the common properties of book with req.body while leaving untouched the others.
      • At the end we export the object using a faster syntax which pairs key and value to avoid useless repetitions.

      We finished this section and actually we have a working app!

      A Naive Test

      Now let’s run the app and open POSTMAN to send HTTP request to the server and check if everything is working as expected.

      in the command line run

      npm start
      

      GET /book

      in POSTMAN run the GET request and, assuming the database contains books, here is the result:

      The server correctly returned the book list in my database.

      POST /book

      Let’s add a book and POST to the server:

      It seems the book was perfectly added. The server returned the book and a message confirming it was added in our bookstore. Is it true? Let’s send another GET request and here is the result:

      Awesome it works!

      PUT /book/:id

      Let’s update a book by changing the page and check the result:

      Great! PUT also seems to be working so let’s send another GET request to check all the list:

      All is running smoothly…

      GET /book/:id

      Now let’s get a single book by sending the id in the GET request and then delete it:

      As it returns the correct book let’s try now to delete it:

      DELETE /book/:id

      Here is the result of the DELETE request to the server:

      Even the last request works smoothly and we do not need to doublecheck with another GET request as we are sending the client some info from mongo (result property) which states the book was actually deleted.

      By doing some test with POSTMAN the app happened to behave as expected right? So, would you shoot it to your clients?

      Let me reply for you: NO!!

      Ours is what I called a naive test because we simply tried few operations without testing strange situations that may happen: A post request without some expected data, a DELETE with a wrong id as parameter or even without id to name few.

      This is obviously a simple app and if we were lucky enough, we coded it without introducing bugs of any sort, but what about a real-world app? Moreover, we spent time to run with POSTMAN some test HTTP requests so what would happen if one day we had to change the code of one of those? Test them all again with POSTMAN? Have you started to realize this is not an agile approach?

      This is nothing but few situations you may encounter and you already encountered in your journey as a developer, luckily we have tools to create tests which are always available and can be launched with a single comman line.

      Let’s do something better to test our app!

      A Better Test

      First, let’s create a file in /test called book.js and paste the following code:

      //During the test the env variable is set to test
      process.env.NODE_ENV = 'test';
      
      let mongoose = require("mongoose");
      let Book = require('../app/models/book');
      
      //Require the dev-dependencies
      let chai = require('chai');
      let chaiHttp = require('chai-http');
      let server = require('../server');
      let should = chai.should();
      
      
      chai.use(chaiHttp);
      //Our parent block
      describe('Books', () => {
          beforeEach((done) => { //Before each test we empty the database
              Book.remove({}, (err) => { 
                 done();           
              });        
          });
      /*
        * Test the /GET route
        */
        describe('/GET book', () => {
            it('it should GET all the books', (done) => {
              chai.request(server)
                  .get('/book')
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('array');
                        res.body.length.should.be.eql(0);
                    done();
                  });
            });
        });
      
      });
      

      Wow that’s a lot of new things, let’s dig into it:

      1. You must have noticed the we set the NODE_ENV variable to test, by doing so we change the configuration file to be loaded so the server will connect to the test database and avoid morgan logs in the cmd.
      2. We required the dev-dependencies modules and server itself (Do you remember we exported it by module.exports?).

      3. We defined should by running chai.should() to style our tests on the HTTP requests result, then we told chai to use chai HTTP.

      So it starts with “describe” blocks of code for better organizing your assertions and this organization will reflect in the output at command line as we will see later.

      beforeEach is a block of code that is going to run before each the describe blocks on the same level. Why we did that? Well we are going to remove any book from the database to start with an empty bookstore whenever a test is run.

      Test the /GET route

      And here it comes the first test, chai is going to perform a GET request to the server and the assertions on the res variable will satisfy or reject the first parameter of the the it block it should GET all the books. Precisely, given the empty bookstore the result of the request should be:

      1. Status 200.
      2. The result should be an array.
      3. Since the bookstore is empty, we presumed the length is equal to 0.

      Notice that the syntax of should assertions is very intituitive as it is similar as a natural language statement.

      Now, in the command line run:

      “`javascript npm test ”`

      and here it is the output:

      The test passed and the output reflects the way we organized our code with blocks of describe.

      Test the /POST route

      Now let’s check our robust is our API, suppose we are trying to add a book with missing pages field passed to the server: The server should not respond with a proper error message.

      Copy and paste the following code in the test file:

      process.env.NODE_ENV = 'test';
      
      let mongoose = require("mongoose");
      let Book = require('../app/models/book');
      
      let chai = require('chai');
      let chaiHttp = require('chai-http');
      let server = require('../server');
      let should = chai.should();
      
      
      chai.use(chaiHttp);
      
      describe('Books', () => {
          beforeEach((done) => {
              Book.remove({}, (err) => { 
                 done();           
              });        
          });
        describe('/GET book', () => {
            it('it should GET all the books', (done) => {
              chai.request(server)
                  .get('/book')
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('array');
                        res.body.length.should.be.eql(0);
                    done();
                  });
            });
        });
        /*
        * Test the /POST route
        */
        describe('/POST book', () => {
            it('it should not POST a book without pages field', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954
                }
              chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('errors');
                        res.body.errors.should.have.property('pages');
                        res.body.errors.pages.should.have.property('kind').eql('required');
                    done();
                  });
            });
      
        });
      });
      
      

      Here we added the test on an incomplete /POST request, let’s analyze the assertions:

      1. Status should be 200.
      2. The response body should be an object.
      3. One of the body properties should be errors.
      4. Errors should have have the missing field pages as property.
      5. Finally pages should have the property kind equal to required in order to highlight the reason why we got a negative answer from the server.

      NB notice that we send the book along with the POST request by the .send() function.

      Let’s run the same command again and here is the output:

      Oh Yeah our test test is correct!

      Before writing a new test let me precise two things:

      1. First of all, why the server response structured that way? If you read the callback function for the /POST route, you will notice that in case of missing required fields, the server sends back the error message from mongoose. Try with POSTMAN and check the response.
      2. In case of missing fields we still return a status of 200, this is for simplicity as we are just learning to test our routes. However I suggest to return a status of 206 Partial Content instead.

      Let’s send a book with all the required fields this time. Copy and paste the following code in the test file:

      process.env.NODE_ENV = 'test';
      
      let mongoose = require("mongoose");
      let Book = require('../app/models/book');
      
      let chai = require('chai');
      let chaiHttp = require('chai-http');
      let server = require('../server');
      let should = chai.should();
      
      
      chai.use(chaiHttp);
      
      describe('Books', () => {
          beforeEach((done) => {
              Book.remove({}, (err) => { 
                 done();           
              });        
          });
        describe('/GET book', () => {
            it('it should GET all the books', (done) => {
              chai.request(server)
                  .get('/book')
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('array');
                        res.body.length.should.be.eql(0);
                    done();
                  });
            });
        });
        /*
        * Test the /POST route
        */
        describe('/POST book', () => {
            it('it should not POST a book without pages field', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954
                }
              chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('errors');
                        res.body.errors.should.have.property('pages');
                        res.body.errors.pages.should.have.property('kind').eql('required');
                    done();
                  });
            });
            it('it should POST a book ', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954,
                    pages: 1170
                }
              chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('message').eql('Book successfully added!');
                        res.body.book.should.have.property('title');
                        res.body.book.should.have.property('author');
                        res.body.book.should.have.property('pages');
                        res.body.book.should.have.property('year');
                    done();
                  });
            });
        });
      });
      
      

      This time we expect a returning object with a message saying we succesfully added the book and the book itself (remember with POSTMAN?). You should be now quite familiar with the assertions I made so there is no need for going into detail. Instead, run the command again and here is the output:

      Smooth~

      Test /GET/:id Route

      Now let’s create a book, save it into the database and use the id to send a GET request to the server. Copy and paste the following code in the test file:

      process.env.NODE_ENV = 'test';
      
      let mongoose = require("mongoose");
      let Book = require('../app/models/book');
      
      let chai = require('chai');
      let chaiHttp = require('chai-http');
      let server = require('../server');
      let should = chai.should();
      
      
      chai.use(chaiHttp);
      
      describe('Books', () => {
          beforeEach((done) => {
              Book.remove({}, (err) => { 
                 done();           
              });        
          });
        describe('/GET book', () => {
            it('it should GET all the books', (done) => {
                  chai.request(server)
                  .get('/book')
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('array');
                        res.body.length.should.be.eql(0);
                    done();
                  });
            });
        });
        describe('/POST book', () => {
            it('it should not POST a book without pages field', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954
                }
                  chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('errors');
                        res.body.errors.should.have.property('pages');
                        res.body.errors.pages.should.have.property('kind').eql('required');
                    done();
                  });
            });
            it('it should POST a book ', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954,
                    pages: 1170
                }
                  chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('message').eql('Book successfully added!');
                        res.body.book.should.have.property('title');
                        res.body.book.should.have.property('author');
                        res.body.book.should.have.property('pages');
                        res.body.book.should.have.property('year');
                    done();
                  });
            });
        });
       /*
        * Test the /GET/:id route
        */
        describe('/GET/:id book', () => {
            it('it should GET a book by the given id', (done) => {
                let book = new Book({ title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 });
                book.save((err, book) => {
                    chai.request(server)
                  .get('/book/' + book.id)
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('title');
                        res.body.should.have.property('author');
                        res.body.should.have.property('pages');
                        res.body.should.have.property('year');
                        res.body.should.have.property('_id').eql(book.id);
                    done();
                  });
                });
      
            });
        });
      });
      
      

      Through the assertions we made sure the server returned all the fields and the right book testing the two ids together. Here is the output:

      Have you noticed that by testing single routes within independent blocks we provide a very clear output? Also, isn’t it so efficient? We wrote several tests that can be repeated with a single command line, once and for all.

      Test the /PUT/:id Route

      Time for testing an update on one of our books, we first save the book and then update the year it was published. So, copy and paste the following code:

      process.env.NODE_ENV = 'test';
      
      let mongoose = require("mongoose");
      let Book = require('../app/models/book');
      
      let chai = require('chai');
      let chaiHttp = require('chai-http');
      let server = require('../server');
      let should = chai.should();
      
      
      chai.use(chaiHttp);
      
      describe('Books', () => {
          beforeEach((done) => {
              Book.remove({}, (err) => { 
                 done();           
              });        
          });
        describe('/GET book', () => {
            it('it should GET all the books', (done) => {
                  chai.request(server)
                  .get('/book')
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('array');
                        res.body.length.should.be.eql(0);
                    done();
                  });
            });
        });
        describe('/POST book', () => {
            it('it should not POST a book without pages field', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954
                }
                  chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('errors');
                        res.body.errors.should.have.property('pages');
                        res.body.errors.pages.should.have.property('kind').eql('required');
                    done();
                  });
            });
            it('it should POST a book ', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954,
                    pages: 1170
                }
                  chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('message').eql('Book successfully added!');
                        res.body.book.should.have.property('title');
                        res.body.book.should.have.property('author');
                        res.body.book.should.have.property('pages');
                        res.body.book.should.have.property('year');
                    done();
                  });
            });
        });
        describe('/GET/:id book', () => {
            it('it should GET a book by the given id', (done) => {
                let book = new Book({ title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 });
                book.save((err, book) => {
                    chai.request(server)
                  .get('/book/' + book.id)
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('title');
                        res.body.should.have.property('author');
                        res.body.should.have.property('pages');
                        res.body.should.have.property('year');
                        res.body.should.have.property('_id').eql(book.id);
                    done();
                  });
                });
      
            });
        });
       /*
        * Test the /PUT/:id route
        */
        describe('/PUT/:id book', () => {
            it('it should UPDATE a book given the id', (done) => {
                let book = new Book({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1948, pages: 778})
                book.save((err, book) => {
                      chai.request(server)
                      .put('/book/' + book.id)
                      .send({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1950, pages: 778})
                      .end((err, res) => {
                            res.should.have.status(200);
                            res.body.should.be.a('object');
                            res.body.should.have.property('message').eql('Book updated!');
                            res.body.book.should.have.property('year').eql(1950);
                        done();
                      });
                });
            });
        });
      });
      
      

      We wanna make sure the message is the correct Book updated! one and that the year field was actually updated. Here is the output:

      Good, we are close to the end, we still gotta test the DELETE route.

      Test the /DELETE/:id Route

      The pattern is similar to the previous tests, we first store a book, delete it and test against the response. Copy and paste the following code:

      process.env.NODE_ENV = 'test';
      
      let mongoose = require("mongoose");
      let Book = require('../app/models/book');
      
      let chai = require('chai');
      let chaiHttp = require('chai-http');
      let server = require('../server');
      let should = chai.should();
      
      
      chai.use(chaiHttp);
      
      describe('Books', () => {
          beforeEach((done) => {
              Book.remove({}, (err) => { 
                 done();           
              });        
          });
        describe('/GET book', () => {
            it('it should GET all the books', (done) => {
                  chai.request(server)
                  .get('/book')
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('array');
                        res.body.length.should.be.eql(0);
                    done();
                  });
            });
        });
        describe('/POST book', () => {
            it('it should not POST a book without pages field', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954
                }
                  chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('errors');
                        res.body.errors.should.have.property('pages');
                        res.body.errors.pages.should.have.property('kind').eql('required');
                    done();
                  });
            });
            it('it should POST a book ', (done) => {
                let book = {
                    title: "The Lord of the Rings",
                    author: "J.R.R. Tolkien",
                    year: 1954,
                    pages: 1170
                }
                  chai.request(server)
                  .post('/book')
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('message').eql('Book successfully added!');
                        res.body.book.should.have.property('title');
                        res.body.book.should.have.property('author');
                        res.body.book.should.have.property('pages');
                        res.body.book.should.have.property('year');
                    done();
                  });
            });
        });
        describe('/GET/:id book', () => {
            it('it should GET a book by the given id', (done) => {
                let book = new Book({ title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 });
                book.save((err, book) => {
                    chai.request(server)
                  .get('/book/' + book.id)
                  .send(book)
                  .end((err, res) => {
                        res.should.have.status(200);
                        res.body.should.be.a('object');
                        res.body.should.have.property('title');
                        res.body.should.have.property('author');
                        res.body.should.have.property('pages');
                        res.body.should.have.property('year');
                        res.body.should.have.property('_id').eql(book.id);
                    done();
                  });
                });
      
            });
        });
        describe('/PUT/:id book', () => {
            it('it should UPDATE a book given the id', (done) => {
                let book = new Book({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1948, pages: 778})
                book.save((err, book) => {
                      chai.request(server)
                      .put('/book/' + book.id)
                      .send({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1950, pages: 778})
                      .end((err, res) => {
                            res.should.have.status(200);
                            res.body.should.be.a('object');
                            res.body.should.have.property('message').eql('Book updated!');
                            res.body.book.should.have.property('year').eql(1950);
                        done();
                      });
                });
            });
        });
       /*
        * Test the /DELETE/:id route
        */
        describe('/DELETE/:id book', () => {
            it('it should DELETE a book given the id', (done) => {
                let book = new Book({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1948, pages: 778})
                book.save((err, book) => {
                      chai.request(server)
                      .delete('/book/' + book.id)
                      .end((err, res) => {
                            res.should.have.status(200);
                            res.body.should.be.a('object');
                            res.body.should.have.property('message').eql('Book successfully deleted!');
                            res.body.result.should.have.property('ok').eql(1);
                            res.body.result.should.have.property('n').eql(1);
                        done();
                      });
                });
            });
        });
      });
      
      

      Again the server returns a message and properties from mongoose that we assert so let’s check the output:

      Great, our tests are all positive and we have a good basis to continue testing our routes with more sophisticated assertions.

      Congratulation for completing the tutorial!

      Conclusion

      In this tutorial we faced the problem of testing our routes to provide our users a stable experience.

      We went through all the steps of creating a RESTful API, doing a naive test with POSTMAN and then propose a better way to test, in fact the main topic of the tutorial.

      It is good habit to always spend some time making tests to assure a server as reliable as possible but unfortunately it is often underestimated.

      During the tutorial we also discuss a few benefits of code testing and this will open doors to more advanced topics such as Test Driven Development (TDD).



      Source link

      So testen Sie ein Node.js-Modul mit Mocha und Assert


      Die Autorin wählte den Open Internet/Free Speech Fund, um eine Spende im Rahmen des Programms Write for DOnations zu erhalten.

      Einführung

      Testen ist ein integraler Bestandteil der Softwareentwicklung. Es ist üblich, dass Programmierer Code ausführen, der ihre Anwendung testet, während sie darin Änderungen vornehmen. So können sie bestätigen, dass sich die Anwendung so verhält, wie sie es gerne hätten. Mit dem richtigen Test-Setup kann dieser Prozess sogar automatisiert sein und somit eine Menge Zeit sparen. Das Ausführen von Tests nach dem Schreiben von neuem Code stellt sicher, dass neue Änderungen keine bereits vorhandenen Funktionen brechen. Das gibt dem Entwickler Vertrauen in seine Code-Basis, insbesondere dann, wenn der Code produktiv eingesetzt wird, damit die Benutzer mit ihm interagieren können.

      Ein Test-Framework strukturiert die Art, wie wir Testfälle erstellen. Mocha ist ein beliebtes JavaScript-Framework, das unsere Testfälle organisiert und für uns ausführt. Mocha verifiziert jedoch nicht das Verhalten unseres Codes. Um Werte in einem Test zu vergleichen, können wir das Node.js-assert-Modul verwenden.

      In diesem Artikel schreiben Sie Tests für ein Node.js-TODO-Listenmodul. Sie richten das Testframework von Mocha ein und nutzen es, um Ihre Tests zu strukturieren. Dann verwenden Sie das Node.js-assert-Modul, um die Tests selbst zu erstellen. In diesem Sinne verwenden Sie Mocha als Planersteller und assert zur Umsetzung des Plans.

      Voraussetzungen

      Schritt 1 — Schreiben eines Node-Moduls

      Beginnen wir diesen Artikel mit dem Schreiben des Node.js-Moduls, das wir testen möchten. Dieses Modul verwaltet eine Liste von TODO-Elementen. Mithilfe dieses Moduls können wir alle TODOs auflisten, die wir verfolgen, sowie neue Elemente hinzufügen und einige als abgeschlossen markieren. Zusätzlich können wir eine Liste von TODO-Elementen in eine CSV-Datei exportieren. Wenn Sie eine Auffrischung über das Schreiben von Node.js-Modulen wünschen, können Sie unseren Artikel Erstellen eines Node.js-Moduls lesen.

      Zuerst müssen wir die Codierungsumgebung einrichten. Erstellen Sie einen Ordner mit dem Namen Ihres Projekts in Ihrem Terminal. Dieses Tutorial verwendet den Namen todos:

      Gehen Sie dann in diesen Ordner hinein:

      Initialisieren Sie nun npm, da wir später zum Ausführen der Tests seine CLI-Funktionalität verwenden:

      Wir haben nur eine Abhängigkeit, Mocha, das wir zur Organisation und Durchführung unserer Tests verwenden. Zum Herunterladen und Installieren von Mocha verwenden Sie Folgendes:

      • npm i request --save-dev mocha

      Wir installieren Mocha als dev-Abhängigkeit, da es für das Modul in einer Produktionsumgebung nicht erforderlich ist. Wenn Sie mehr über Node.js-Packages oder npm erfahren möchten, lesen Sie den Leitfaden Verwenden von Node.js-Modulen mit npm und package.json.

      Abschließend erstellen wir unsere Datei, die den Code unseres Moduls enthalten wird:

      Damit können wir nun unser Modul erstellen. Öffnen Sie index.js in einem Texteditor wie nano:

      Zuerst definieren wir die Todos-Klasse. Diese Klasse enthält alle Funktionen, die wir zur Verwaltung unserer TODO-Liste benötigen. Fügen Sie index.js die folgenden Zeilen von Code hinzu:

      todos/index.js

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

      Wir beginnen mit der Datei, indem wir eine Todos-Klasse erstellen. Seine Funktion constructor() nimmt keine Argumente an, daher müssen wir keine Werte bereitstellen, um ein Objekt für diese Klasse zu instanziieren. Wenn wir ein Todos-Objekt initialisieren, erstellen wir lediglich eine todos-Funktion, bei der es sich um ein leeres Array handelt.

      Die modules-Zeile ermöglicht es anderen Node.js-Modulen, unsere Todos-Klasse zu verlangen. Wenn wir sie nicht ausdrücklich exportieren, könnte die Testdatei, die wir später erstellen, sie nicht verwenden.

      Wir fügen nun eine Funktion hinzu, um das Array von todos, das wir gespeichert haben, auszugeben. Fügen Sie die folgenden hervorgehobenen Zeilen ein:

      todos/index.js

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

      Unsere list()-Funktion gibt eine Kopie des Arrays aus, die von der Klasse verwendet wird. Sie erstellt die Kopie des Arrays mit der destrukturierten Syntax von JavaScript. Wir erstellen eine Kopie des Arrays, damit die Änderungen, die der Benutzer an dem von list() ausgegebenen Array vornimmt, nicht das vom Todos-Objekt verwendete Array beeinträchtigen.

      Anmerkung: JavaScript-Arrays sind Referenztypen. Das bedeutet, dass sich JavaScript bei jeder Variablenzuweisung an ein Array oder bei Funktionsaufrufen mit einem Array als Parameter auf das ursprüngliche Array bezieht, das erstellt wurde. Wenn wir zum Beispiel ein Array mit drei Elementen namens x haben und eine neue Variable y erstellen, sodass y = x, y und x sich beide auf dieselbe Sache beziehen. Alle Änderungen, die wir im Array an y vornehmen, wirken sich auf die Variable x aus und umgekehrt.

      Schreiben wir nun die add()-Funktion, die ein neues TODO-Element hinzufügt:

      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;
      

      Unsere add()-Funktion nimmt eine Zeichenfolge und platziert sie bei einem neuen JavaScript-Object in die title-Funktion. Das neue Objekt hat auch eine completed-Funktion, die standardmäßig auf false gesetzt ist. Dann fügen wir unser neues Objekt unserem Array von TODOs hinzu.

      Eine wichtige Funktionalität in einem TODO-Manager ist die Markierung von Elementen als abgeschlossen. Um dies umzusetzen, durchlaufen wir unser todos-Array, um das TODO-Element zu finden, nach dem der Benutzer sucht. Wenn eines gefunden wird, markieren wir es als abgeschlossen. Wenn keines gefunden wird, geben wir einen Fehler aus.

      Fügen Sie die complete()-Funktion wie hier gezeigt hinzu:

      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;
      

      Speichern Sie die Datei und beenden Sie den Texteditor.

      Wir verfügen nun über einen grundlegenden TODO-Manager, mit dem wir experimentieren können. Als Nächstes testen wir den Code manuell, um zu sehen, ob die Anwendung funktioniert.

      Schritt 2 — Manuelles Testen des Codes

      In diesem Schritt führen wir die Funktionen unseres Codes aus und beobachten die Ausgabe, um sicherzustellen, dass sie unseren Erwartungen entspricht. Das nennt sich manuelles Testen. Es ist wahrscheinlich die gebräuchlichste Testmethodik, die Programmierer anwenden. Obwohl wir unsere Tests später mit Mocha automatisieren, testen wir unseren Code zunächst manuell, um ein besseres Gefühl dafür zu bekommen, wie sich manuelles Testen von Test-Frameworks unterscheidet.

      Wir fügen unserer Anwendung zwei TODO-Elemente hinzu und markieren eines als abgeschlossen. Starten Sie die Node.js REPL in dem gleichen Ordner wie die Datei index.js:

      Sie sehen die Eingabeaufforderung > in der REPL, die uns anzeigt, dass wir JavaScript-Code eingeben können. Geben Sie Folgendes an der Eingabeaufforderung ein:

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

      Mit require() laden wir das TODOs-Modul in eine Todos-Variable. Erinnern Sie sich, dass unser Modul die Todos-Klasse standardmäßig ausgibt.

      Lassen Sie uns nun ein Objekt für diese Klasse instanziieren. Fügen Sie diese Zeile von Code in der REPL hinzu:

      • const todos = new Todos();

      Wir können das todos-Objekt verwenden, um unsere Umsetzungsarbeiten zu verifizieren. Fügen wir unser erstes TODO-Element hinzu:

      Bisher haben wir noch keine Ausgabe in unserem Terminal gesehen. Verifizieren wir, dass wir unser "run code"-TODO-Element gespeichert haben, indem wir eine Liste aller unserer TODOs abrufen:

      Sie sehen diese Ausgabe in Ihrer REPL:

      Output

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

      Das ist das erwartete Ergebnis: Wir haben ein TODO-Element in unserem Array von TODOs und es ist nicht standardmäßig abgeschlossen.

      Wir fügen ein weiteres TODO-Element hinzu:

      • todos.add("test everything");

      Markieren Sie das erste TODO-Element als abgeschlossen:

      • todos.complete("run code");

      Unser todos-Objekt verwaltet nun zwei Elemente: "run code" und "test everything". Das "run code"-TODO wird ebenfalls abgeschlossen sein. Bestätigen wir dies, indem wir list() erneut aufrufen:

      Die REPL wird Folgendes ausgeben:

      Output

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

      Beenden Sie nun die REPL wie folgt:

      Wir haben bestätigt, dass sich unser Modul erwartungsgemäß verhält. Wir haben unseren Code nicht in eine Testdatei gestellt oder eine Testbibliothek verwendet, sondern manuell getestet. Leider ist diese Testform zeitaufwendig, wenn wir sie bei jeder Änderung, die wir vornehmen, durchführen. Als Nächstes verwenden wir automatisiertes Testen in Node.js und sehen, ob wir dieses Problem mit dem Mocha lösen können.

      Schritt 3 — Schreiben des ersten Tests mit Mocha und Assert

      Im letzten Schritt haben wir unsere Anwendung manuell getestet. Das funktioniert für individuelle Anwendungsfälle, aber wenn unser Modul skaliert, wird diese Methode weniger praktikabel. Wenn wir neue Eigenschaften testen, müssen wir sicher sein, dass die hinzugefügte Funktionalität keine Probleme in der alten Funktionalität verursacht. Wir möchten jede Eigenschaft bei jeder Änderung des Codes erneut testen, aber dies von Hand zu tun wäre sehr aufwendig und fehleranfällig.

      Eine effizientere Praxis wäre die Einrichtung automatisierter Tests. Das sind skriptbasierte Tests, die wie jeder andere Codeblock geschrieben sind. Wir führen unsere Funktionen mit definierten Eingaben aus und inspizieren ihre Effekte, um sicherzustellen, dass sie sich wie erwartet verhalten. Mit dem Anwachsen unserer Codebasis wächst auch der Umfang der automatisierten Tests. Wenn wir neue Tests zusammen mit den Eigenschaften schreiben, können wir überprüfen, ob das gesamte Modul noch funktioniert – ohne sich jedesmal daran erinnern zu müssen, wie jede einzelne Funktion genutzt wird.

      In diesem Tutorial verwenden wir das Testframework von Mocha mit dem Node.js-assert-Modul. Wir sammeln ein paar praktische Erfahrungen, um zu sehen, wie sie zusammenarbeiten.

      Erstellen Sie zunächst eine neue Datei, um unseren Testcode zu speichern:

      Verwenden Sie nun Ihren bevorzugten Texteditor, um die Testdatei zu öffnen. Sie können wie zuvor nano verwenden:

      In der ersten Zeile der Textdatei laden wir das TODOs-Modul, so wie wir es mit der Node.js-Shell getan haben. Dann laden wir das assert-Modul für das Schreiben unserer Tests. Fügen Sie die folgenden Zeilen hinzu:

      todos/index.test.js

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

      Die strict-Funktion des assert-Moduls erlaubt uns, spezielle Gleichheitstests anzuwenden, die von Node.js empfohlen werden und gut für das zukünftige Prüfen geeignet sind, da sie mehr Anwendungsfälle berücksichtigen.

      Bevor wir mit dem Schreiben von Tests beginnen, behandeln wir, wie Mocha unseren Code organisiert. Die in Mocha strukturierten Tests folgen normalerweise dieser Vorlage:

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

      Beachten Sie zwei Schlüsselfunktionen: describe() und it(). Die describe()-Funktion wird zur Gruppierung ähnlicher Tests genutzt. Es ist nicht erforderlich, für Mocha Tests auszuführen, aber die Gruppierung von Tests erleichtert die Pflege unseres Testcodes. Es wird empfohlen, Ihre Tests so zu gruppieren, dass Sie ähnliche Tests leicht zusammen aktualisieren können.

      Die Funktion it() enthält unseren Testcode. Hier würden wir mit den Funktionen unseres Moduls interagieren und die assert-Bibliothek verwenden. Viele it()-Funktionen können als describe()-Funktion definiert werden.

      Unser Ziel in diesem Abschnitt ist die Verwendung von Mocha und assert zur Automatisierung unserer manuellen Tests. Wir führen dies Schritt für Schritt aus und beginnen mit unserem Beschreibungsblock. Fügen Sie Folgendes nach den Modulzeilen in Ihre Datei ein:

      todos/index.test.js

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

      Mit diesem Codeblock haben wir eine Gruppierung für unsere integrierten Tests erstellt. Komponententests würden jeweils nur eine Funktion testen. Integrationstests verifizieren, wie gut Funktionen innerhalb oder über Module zusammenarbeiten. Wenn Mocha unseren Test ausführt, laufen alle Tests innerhalb des Beschreibungsblocks unter der "integration test"-Gruppe.

      Wir fügen nun eine it()-Funktion hinzu, damit wir mit dem Testen des Codes unseres Moduls beginnen können:

      todos/index.test.js

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

      Beachten Sie, wie deskriptiv wir den Namen des Tests gemacht haben. Wenn jemand unseren Test ausführt, wird sofort klar, was passiert oder fehlgeschlagen ist. Eine gut getestete Anwendung ist typischerweise eine gut dokumentierte Anwendung und Tests können manchmal eine effektive Art der Dokumentation sein.

      Für unseren ersten Test erstellen wir ein neues Todos-Objekt und verifizieren, dass es keine Elemente enthält:

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

      Die erste neue Zeile des Codes instanziierte ein neues Todos-Objekt, wie wir es in der Node.js-REPL oder einem anderen Modul tun würden. In der zweiten neuen Zeile verwenden wir das assert-Modul.

      Aus dem assert-Modul verwenden wir die notStrictEqual()-Methode. Diese Funktion nimmt zwei Parameter: den Wert, den wir testen möchten (genannt actual-Wert) und den Wert, den wir erhalten möchten (genannt expected-Wert). Wenn beide Argumente gleich sind, gibt notStrictEqual() einen Fehler aus, damit der Test fehlschlägt.

      Speichern und beenden Sie index.test.js.

      Der Basisfall wird wahr sein, da die Länge 0 sein sollte, was nicht 1 ist. Bestätigen wir das, indem wir Mocha ausführen. Dazu müssen wir unsere package.json-Datei ändern. Öffnen Sie Ihre package.json-Datein mit Ihrem Texteditor:

      Ändern Sie diese nun in der scripts-Funktion so wie hier gezeigt:

      todos/package.json

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

      Wir haben nun das Verhalten des npm-CLI-Befehls test geändert. Wenn wir npm test ausführen, überprüft npm den gerade eingegebenen Befehl in package.json. Die Ausführung sucht nach der Mocha-Bibliothek in unserem node_modules-Ordner und führt den mocha-Befehl mit unserer Testdatei aus.

      Speichern und beenden Sie package.json.

      Nun sehen wir uns an, was passiert, wenn wir unseren Test ausführen. Geben Sie Folgendes in Ihr Terminal ein:

      Der Befehl erzeugt die folgende Ausgabe:

      Output

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

      Diese Ausgabe zeigt uns zunächst, welche Testgruppe sie nun ausführen wird. Für jeden einzelnen Test innerhalb einer Gruppe ist der Testfall einbezogen. Wir sehen unseren Testnamen wie in der Funktion it() beschrieben. Das Häkchen auf der linken Seite des Testfalls zeigt an, dass der Test bestanden ist.

      Am Ende erhalten wir eine Zusammenfassung aller Tests. In unserem Fall ist unser einzelner Test bestanden und wurde in 16 ms abgeschlossen (die Zeit variiert von Computer zu Computer).

      Unsere Testung hat erfolgreich begonnen. Der aktuelle Testfall kann jedoch falsch-positive Meldungen liefern. Ein falsch-positiver Testfall ist ein Testfall, der bestanden wird, wenn er fehlschlagen sollte.

      Wir überprüfen gerade, dass die Länge des Arrays nicht gleich 1 ist. Wir werden den Test nun so ändern, dass dieser Zustand auch dann zutrifft, wenn er es nicht sollte. Fügen Sie der index.test.js folgende Zeilen hinzu:

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

      Speichern und schließen Sie die Datei.

      Wir haben zwei TODO-Elemente hinzugefügt. Wir führen nun den Test aus, um zu sehen, was passiert:

      Dadurch ergibt sich Folgendes:

      Output

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

      Wie ewartet besteht der Test, da die Länge größer als 1 ist. Jedoch wird der ursprüngliche Zweck des ersten Tests verfehlt. Der erste Test ist dazu gedacht, zu bestätigen, dass wir mit einem Leerzustand beginnen. Ein besserer Test bestätigt dies in allen Fällen.

      Wir ändern nun den Test, damit er nur dann bestanden wird, wenn wir absolut keine TODOs im Speicher haben. Führen Sie die folgenden Änderungen in der index.test.js aus:

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

      Sie haben notStrictEqual() auf strictEqual() geändert, eine Funktion, die die Gleichheit zwischen dem tatsächlichen und erwarteten Argument überprüft. Die Strict-Equal-Funktion schlägt fehl, wenn unsere Argumente nicht genau gleich sind.

      Speichern und beenden Sie und führen Sie dann den Test aus, damit wir sehen können, was passiert:

      Dieses Mal zeigt die Ausgabe einen Fehler:

      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.

      Dieser Text wird uns helfen, herauszufinden, warum der Test fehlgeschlagen ist. Beachten Sie, dass zu Beginn des Testfalls kein Häkchen vorhanden ist, da der Test fehlgeschlagen ist.

      Unsere Testzusammenfassung befindet sich nicht mehr am Ende der Ausgabe, sondern direkt nach der Anzeige unserer Liste von Testfällen:

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

      Die verbleibende Ausgabe gibt uns Daten über unsere fehlgeschlagenen Tests. Zuerst sehen wir, welcher Testfall fehlgeschlagen ist:

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

      Dann sehen wir, warum unser Test fehlgeschlagen ist:

      ...
            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)
      ...
      

      Es wird ein AssertionError gemeldet, wenn strictEqual() fehlschlägt. Wir sehen, dass der expected-Wert, 0, vom actual-Wert, 2, abweicht.

      Dann sehen wir die Zeile in unserer Testdatei, in der der Code fehlschlägt. In diesem Fall ist es Zeile 10.

      Nun haben wir selbst gesehen, dass unser Test fehlschlägt, wenn wir fehlerhafte Werte erwarten. Wir ändern unseren Testfall wieder auf seinen richtigen Wert. Öffnen Sie die Datei:

      Dann nehmen Sie die todos.add-Zeilen heraus, sodass Ihr Code wie folgt aussieht:

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

      Speichern und schließen Sie die Datei.

      Führen Sie ihn erneut aus, um zu bestätigen, dass er ohne potenzielle falsch-positive Meldungen besteht:

      Sie erhalten folgende Ausgabe:

      Output

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

      Wir haben nun die Belastbarkeit unseres Tests deutlich verbessert. Fahren wir mit unserem Integrationstest fort. Der nächste Schritt ist das Hinzufügen eines neuen TODO-Elements in 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}]);
          });
      });
      

      Nach der Verwendung der add()-Funktion bestätigen wir, dass wir nun ein TODO haben, das von unserem todos-Objekt mit strictEqual() verwaltet wird. Unser nächster Test bestätigt die Daten in den todos mit deepStrictEqual(). Die Funktion deepStrictEqual() prüft rekursiv, ob unsere erwarteten und tatsächlichen Objekte die gleichen Eigenschaften haben. In diesem Fall testet sie, dass die von uns erwarteten Arrays beide ein JavaScript-Objekt beinhalten. Dann überprüft sie, dass ihre JavaScript-Objekte die gleichen Eigenschaften haben, d. h., ihre beiden title-Eigenschaften "run code" und ihre beiden completed-Eigenschaften false sind.

      Dann schließen wir die restlichen Tests unter Verwendung dieser beiden Gleichheitsprüfungen nach Bedarf durch Hinzufügen der folgenden hervorgehobenen Zeilen ab:

      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 }
                  ]
          );
        });
      });
      

      Speichern und schließen Sie die Datei.

      Unser Test imitiert nun unseren manuellen Test. Mit diesen programmatischen Tests müssen wir die Ausgabe nicht kontinuierlich überprüfen, um zu sehen, ob unsere Tests bei der Ausführung bestehen. Üblicherweise möchte man jeden Aspekt der Verwendung testen, um sicherzustellen, dass der Code ordnungsgemäß getestet wird.

      Wir führen unseren Test erneut mit npm test aus, um diese bekannte Ausgabe zu erhalten:

      Output

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

      Sie haben nun einen integrierten Test mit dem Mocha-Framework und der assert-Bibliothek eingerichtet.

      Gehen wir nun von einer Situation aus, in der wir unser Modul mit einigen anderen Entwicklern geteilt haben und diese uns jetzt Feedback geben. Viele unserer Benutzer würden sich wünschen, dass die Funktion complete() einen Fehler meldet, wenn bisher noch keine TODOs hinzugefügt wurden. Wir fügen diese Funktionalität in unserer Funktion complete() ein.

      Öffnen Sie index.js in Ihrem Texteditor:

      Fügen Sie der Funktion Folgendes hinzu:

      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}"`);
          }
      }
      ...
      

      Speichern und schließen Sie die Datei.

      Nun fügen wir einen neuen Test für diese neue Eigenschaft hinzu. Wir wollen verifizieren, ob ein Todos-Objekt, das keine Elemente enthält, unseren speziellen Fehler ausgibt, wenn wir es mit complete aufrufen.

      Gehen Sie in die index.test.js zurück:

      Fügen Sie am Ende der Datei den folgenden Code hinzu:

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

      Wie zuvor verwenden wir describe() und it(). Wir beginnen unseren Test mit der Erstellung eines neuen todos-Objekts. Dann definieren wir den Fehler, dessen Meldung wir erwarten, wenn wir die Funktion complete() aufrufen.

      Als Nächstes verwenden wir die Funktion throws() des assert-Moduls. Diese Funktion wurde erstellt, damit wir die Fehler, die unser Code ausgibt, verifizieren können. Sein erstes Argument ist eine Funktion, die den Code enthält, der den Fehler ausgibt. Das zweite Argument ist der Fehler, dessen Meldung wir erwarten.

      Führen Sie in Ihrem Terminal erneut die Tests mit npm test aus und Sie sehen die folgende Ausgabe:

      Output

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

      Diese Ausgabe zeigt den Nutzen, warum wir automatisiertes Testen mit Mocha und assert durchführen. Da unsere Tests schriftlich ausgearbeitet sind, verifizieren wir bei jeder Ausführung von npm test, dass alle unsere Tests bestehen. Wir mussten nicht manuell überprüfen, ob der andere Code noch funktioniert – wir wissen, dass es so ist, da der Test, den wir haben, bestand.

      Bisher haben unsere Tests die Ergebnisse von synchronem Code verifiziert. Wir behandeln nun, wie wir unsere neu gewonnenen Testgewohnheiten anpassen müssten, um mit asynchronem Code arbeiten zu können.

      Schritt 4 – Testen von asynchronem Code

      Eine der Eigenschaften, die wir in unserem TODO-Modul benötigen, ist eine CSV-Exportfunktion. Damit werden alle gespeicherten TODOs zusammen mit dem abgeschlossenen Status in einer Datei ausgegeben. Das erfordert die Verwendung des Moduls fs – eines integrierten Node.js-Moduls für die Arbeit mit dem Dateisystem.

      Das Schreiben in eine Datei ist eine asynchrone Operation. Es gibt viele Möglichkeiten, in eine Datei in Node.js zu schreiben. Wir können Callbacks, Promises oder die Schlüsselworte async/await verwenden. In diesem Abschnitt behandeln wir, wie wir Tests für diese verschiedenen Methoden schreiben.

      Callbacks

      Eine callback-Funktion ist eine Funktion, die als Argument in einer asynchronen Funktion verwendet wird. Sie wird aufgerufen, wenn die asynchrone Operation abgeschlossen ist.

      Wir fügen unserer Todos-Klasse eine Funktion namens saveToFile() hinzu. Diese Funktion erstellt eine Zeichenfolge, indem Sie alle unsere TODO-Elemente durchläuft, und schreibt diese Zeichenfolge in eine Datei.

      Öffnen Sie Ihre index.js-Datei:

      Fügen Sie den folgenden hervorgehobenen Code in die Datei ein:

      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;
      

      Zunächst müssen wir das fs-Modul in unsere Datei importieren. Dann haben wir unsere neue Funktion saveToFile() hinzugefügt. Unsere Funktion übernimmt eine Callback-Funktion, die genutzt wird, sobald die Schreiboperation der Datei abgeschlossen ist. In dieser Funktion erstellen wir eine fileContents-Variable, die die gesamte zu speichernde Zeichenfolge als Datei speichert. Sie wird mit den CSV-Titeln initialisiert. Dann durchlaufen wir jedes TODO-Element mit der forEach()-Methode des internen Arrays. Beim Durchlaufen fügen wir die title– und completed-Eigenschaften der einzelnen todos hinzu.

      Zum Schluss verwenden wir das fs-Modul zum Schreiben der Datei mit der writeFile()-Funktion. Unser erstes Argument ist der Dateiname: todos.csv. Das zweite ist der Inhalt der Datei, in diesem Fall unsere fileContents-Variable. Das letzte Argument ist unsere Callback-Funktion, die alle Schreibfehler der Datei behandelt.

      Speichern und schließen Sie die Datei.

      Wir schreiben nun einen Test für unsere Funktion saveToFile. Unser Test führt zwei Dinge aus: Er überprüft die Existenz der Datei und verifiziert, dass sie den richtigen Inhalt hat.

      Öffnen Sie die Datei index.test.js:

      Beginnen wir damit, das fs-Modul am Anfang der Datei zu laden, da wir es zum Testen unserer Ergebnisse verwenden werden:

      todos/index.test.js

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

      Am Ende der Datei fügen wir unseren neuen Testfall hinzu:

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

      Wie zuvor verwenden wir describe(), um unseren Test getrennt von den anderen zu gruppieren, da er eine neue Funktionalität enthält. Die it()-Funktion unterscheidet sich leicht von unseren anderen. Normalerweise hat die von uns verwendete Callback-Funktion keine Argumente. Dieses Mal haben wir done als Argument. Wir benötigen dieses Argument, wenn wir Funktionen mit Callbacks testen. Die Callback-Funktion done() wird von Mocha verwendet, um ihr anzugeben, wenn eine asynchrone Funktion abgeschlossen ist.

      Alle in Mocha getesteten Callback-Funktionen müssen den Callback done() aufrufen. Wäre dies nicht der Fall, würde Mocha nie wissen, ob die Funktion abgeschlossen ist und würde festgefahren auf ein Signal warten.

      Wir erstellen nun unsere Todos-Instanz und fügen ihr ein einzelnes Element hinzu. Wir rufen die Funktion saveToFile() mit einem Callback auf, der einen Dateischreibfehler findet. Beachten Sie, wie unser Test für diese Funktion im Callback enthalten ist. Wenn unser Testcode außerhalb des Callbacks wäre, würde er fehlschlagen, solange der Code aufgerufen würde, bevor das Schreiben der Datei abgeschlossen wäre.

      In unserer Callback-Funktion überprüfen wir zunächst, dass unsere Datei existiert:

      todos/index.test.js

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

      Die Funktion fs.existsSync() gibt true aus, wenn der Dateipfad in ihrem Argument existiert, und andernfalls false.

      Anmerkung: Die Funktionen des fs-Moduls sind standardmäßig asynchron. Sie bildeten jedoch für Schlüsselfunktionen synchrone Gegenstücke. Dieser Test ist einfacher, wenn synchrone Funktionen verwendet werden, da wir den asynchronen Code nicht schachteln müssen, um sicherzustellen, dass er funktioniert. Im fs-Modul enden synchrone Funktionen normalerweise mit "Sync" am Ende ihrer Namen.

      Dann erstellen wir eine Variable, um unseren erwarteten Wert zu speichern:

      todos/index.test.js

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

      Wir verwenden readFileSync() des fs-Moduls zum synchronen Lesen der Datei:

      todos/index.test.js

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

      Wir geben readFileSync() den richtigen Pfad für die Datei: todos.csv. Da readFileSync() ein Buffer-Objekt ausgibt, das Binärdaten speichert, verwenden wir seine toString()-Methode, damit wir seinen Wert mit der Zeichenfolge vergleichen können, die wir voraussichtlich gespeichert haben.

      Wie zuvor verwenden wir das strictEqual des assert-Moduls, um einen Vergleich durchzuführen:

      todos/index.test.js

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

      Wir beenden unseren Test durch das Aufrufen des done()-Callbacks, sodass Mocha weiß, dass der Test dieses Falls gestoppt wird:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Wir geben das err-Objekt zu done(), sodass der Test mit Mocha fehlschlägt, falls ein Fehler vorhanden ist.

      Speichern und beenden Sie index.test.js.

      Wie zuvor führen wir diesen Test mit npm test durch. Ihre Konsole zeigt dann diese Ausgabe:

      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)

      Sie haben nun Ihre erste asynchrone Funktion mit Mocha unter der Verwendung von Callbacks getestet. Zum Zeitpunkt des Schreibens dieses Tutorials sind Promises jedoch verbreiteter als Callbacks in neuem Node.js-Code, wie auch in unserem Artikel Schreiben von asynchronem Code in Node.js beschrieben. Als Nächstes lernen wir, wie wir auch diese mit Mocha testen können.

      Promises

      Ein Promise ist ein JavaScript-Objekt, das letztendlich einen Wert ausgibt. Wenn ein Promise erfolgreich ist, ist es gelöst. Wenn es auf einen Fehler trifft, wird es verworfen.

      Wir ändern die saveToFile()-Funktion, damit sie Promises anstelle von Callbacks verwendet. Öffnen Sie index.js:

      Zuerst müssen wir ändern, wie das fs-Modul geladen ist. Ändern Sie in Ihrer index.js-Datei die require()-Aussage am Anfang der Datei, sodass sie aussieht wie folgt:

      todos/index.js

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

      Wir haben nun das fs-Modul importiert, das Promises anstelle von Callbacks verwendet. Nun müssen wir einige Änderungen an saveToFile() vornehmen, damit es stattdessen mit Promises arbeitet.

      Führen Sie in Ihrem Texteditor die folgenden Änderungen an der Funktion saveToFile() aus, um die Callbacks zu entfernen:

      todos/index.js

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

      Der erste Unterschied besteht darin, dass unsere Funktion keine Argumente mehr akzeptiert. Mit Promises benötigen wir keine Callback-Funktion. Die zweite Änderung betrifft die Weise, wie die Datei geschrieben ist. Wir geben nun das Ergebnis des writeFile()-Promises aus.

      Speichern und schließen Sie index.js.

      Wir passen unseren Test so an, dass er mit Promises funktioniert. Öffnen Sie index.test.js:

      Ändern Sie den saveToFile()-Test auf Folgendes:

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

      Als erste Änderung müssen wir den done()-Callback aus den Argumenten entfernen. Wenn Mocha das done()-Argument durchläuft, muss es aufgerufen werden oder es gibt einen Fehler wie folgt aus:

      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)
      

      Schließen Sie beim Testen von Promises den done()-Callback nicht in it() ein.

      Um unser Promise zu testen, müssen wir unseren Assertionscode in die then()-Funktion einfügen. Beachten Sie, dass wir dieses Promise im Test ausgeben. Wir haben keine catch()-Funktion, um das Promise auszufangen, wenn es verworfen wird.

      Wir geben das Promise aus, sodass alle Fehler, die in der then()-Funktion ausgegeben werden, in der it()-Funktion heraustreten. Wenn die Fehler nicht heraustreten, wird der Testfall mit Mocha nicht fehlschlagen. Beim Testen von Promises müssen Sie return auf das getestete Promise verwenden. Andernfalls besteht das Risiko, ein falsch-positives Ergebnis zu erhalten.

      Wir lassen auch die catch()-Klausel aus, da Mocha erkennen kann, wenn ein Promise verworfen wird. Im Fall einer Verwerfung schlägt der Test automatisch fehl.

      Da unser Test nun fertig ist, speichern und beenden Sie die Datei und führen Sie anschließend Mocha mit npm test aus. Als Bestätigung erhalten wir ein erfolgreiches Ergebnis:

      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)

      Wir haben unseren Code und Test zur Verwendung von Promises geändert und wissen nun sicher, dass es funktioniert. Die neuesten asynchronen Muster verwenden jedoch async/await-Schlüsselwörter, damit wir nicht mehrere then()-Funktionen erstellen müssen, um erfolgreiche Ergebnisse zu bearbeiten. Sehen wir als Nächstes, wie wir mit async/await testen können.

      async/await

      Die Schlüsselwörter async/await erleichtern die Arbeit mit Promises, da sie nicht so ausführlich sind. Sobald wir eine Funktion als asynchron mit dem Schlüsselwort async definieren, können wir alle zukünftigen Ergebnisse in dieser Funktion mit dem Schlüsselwort await erhalten. Auf diese Weise können wir Promises verwenden, ohne die Funktionen then() oder catch() verwenden zu müssen.

      Wir können unseren auf Promises basierenden saveToFile()-Test mit async/await vereinfachen. Führen Sie in Ihrem Texteditor diese kleineren Änderungen im saveToFile()-Test in der index.test.js aus:

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

      Die erste Änderung besteht darin, dass die von der it()-Funktion verwendete Funktion jetzt das Schlüsselwort async zur Definierung verwendet. Dadurch können wir das Schlüsselwort await in ihrem Körper verwenden.

      Die zweite Änderung tritt auf, wenn wir saveToFile() aufrufen. Bevor es aufgerufen wird, wird das Schlüsselwort await verwendet. Node.js wird nun warten, bis diese Funktion gelöst ist, bevor es den Test fortsetzt.

      Da wir den Code aus der then()-Funktion in den it()-Funktionskörper verschoben haben, ist unser Funktionscode leichter zu lesen. Die Ausführung dieses Codes mit npm test erzeugt diese Ausgabe:

      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)

      Wir können jetzt asynchrone Funktionen testen, indem wir ein beliebiges von drei asynchronen Paradigmen entsprechend verwenden.

      Wir haben mit dem Testen von synchronem und asynchronem Code mit Mocha schon einen breiten Bereich abgedeckt. Als Nächstes tauchen wir tiefer ein in einige andere Funktionalitäten, die Mocha bietet, um unsere Testerfahrung zu verbessern. Besonders interessant ist hierbei auch, wie Hooks die Testumgebungen verändern können.

      Schritt 5 – Verwenden von Hooks zur Verbesserung von Testfällen

      Hooks sind ein nützlicher Bestandteil von Mocha, der es uns ermöglicht, die Umgebung vor und nach einem Test zu konfigurieren. Wir fügen Hooks typischerweise in einen describe()-Funktionsblock, da diese eine für einige Testfälle spezifische Auf- und Abbaulogik enthalten.

      Mocha bietet vier Hooks, die wir in unseren Tests verwenden können:

      • before: Dieser Hook wird einmal ausgeführt, bevor der erste Test beginnt.
      • beforeEach: Dieser Hook wird vor jedem Testfall ausgeführt.
      • after: Dieser Hook wird einmal ausgeführt, nachdem der letzte Testfall abgeschlossen ist.
      • afterEach: Dieser Hook wird nach jedem Testfall ausgeführt.

      Hooks sind sehr nützlich, wenn wir eine Funktion oder Eigenschaft mehrmals testen, da sie uns erlauben, den Einrichtungscode des Tests (wie das Erstellen des todos-Objekts) vom Assertionscode des Tests zu trennen.

      Um den Wert von Hooks zu sehen, fügen wir dem Testblock saveToFile() weitere Tests hinzu.

      Obwohl wir bestätigt haben, dass wir unsere TODO-Elemente in eine Datei speichern können, haben wir nur ein Element gespeichert. Außerdem wurde das Element nicht als abgeschlossen markiert. Wir fügen weitere Tests hinzu, um sicherzustellen, dass die verschiedenen Aspekte unseres Moduls funktionieren.

      Wir fügen zunächst einen zweiten Test hinzu, um zu bestätigen, dass unsere Datei korrekt gespeichert wird, wenn wir ein abgeschlossenes TODO-Element haben. Öffnen Sie Ihre Datei index.test.js mit Ihrem Texteditor:

      Ändern Sie den letzten Test folgendermaßen:

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

      Der Test ist ähnlich wie zuvor. Die wichtigsten Unterschiede sind, dass wir die complete()-Funktion vor saveToFile() aufrufen, und unsere expectedFileContents nun true anstatt false für den Wert der completed-Kolumne haben.

      Speichern und schließen Sie die Datei.

      Wir führen unseren neuen Test und alle anderen mit npm test aus:

      Dadurch ergibt sich Folgendes:

      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)

      Es funktioniert wie erwartet. Es gibt jedoch Raum für Verbesserungen. Sie müssen ein Todos-Objekt zu Beginn des Tests instanziieren. Beim Hinzufügen von mehr Testfällen wird dies schnell repetitiv und vergeudet Speicherplatz. Außerdem erstellt der Test bei jeder Ausführung eine Datei. Das kann von jemandem, der sich mit dem Modul nicht so gut auskennt, mit einer tatsächlichen Ausgabe verwechselt werden. Es wäre schön, wenn wir unsere Ausgabedateien nach dem Testen bereinigen würden.

      Führen wir nun diese Verbesserungen mit Test-Hooks aus. Wir verwenden den Hook beforeEach(), um unsere Testvorrichtung von TODO-Elementen einzurichten. Eine Testvorrichtung ist jeder konsistente Zustand, der in einem Test verwendet wird. In unserem Fall ist unsere Testvorrichtung ein neues todos-Objekt, dem bereits ein TODO-Element hinzugefügt wurde. Wir verwenden afterEach(), um die vom Test erstellte Datei zu entfernen.

      Führen Sie in index.test.js die folgenden Änderungen an Ihrem letzten Test für saveToFile() aus:

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

      Entschlüsseln wir alle vorgenommenen Änderungen. Wir haben dem Testblock ein beforeEach() hinzugefügt:

      todos/index.test.js

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

      Diese beiden Zeilen Code erstellen ein neues Todos-Objekt, das in jedem unserer Tests verfügbar ist. Mit Mocha verweist das this-Objekt in beforeEach() auf dasselbe this-Objekt in it(). this ist für jeden Codeblock im describe()-Block gleich. Weitere Informationen über this finden Sie in unserem Tutorial Verstehen Sie This, Bind, Call und Apply in JavaScript.

      Diese leistungsstarke gemeinsame Nutzung des Kontexts ist der Grund, warum wir schnell Testvorrichtungen erstellen können, die für unsere beiden Tests funktionieren.

      Dann bereinigen wir unsere CSV-Datei in der afterEach()-Funktion:

      todos/index.test.js

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

      Wenn unser Test fehlgeschlagen ist, hat der Test möglicherweise keine Datei erstellt. Aus diesem Grund überprüfen wir, ob die Datei vorhanden ist, bevor wir die Funktion unlinkSync() verwenden, um diese zu löschen.

      Die verbleibenden Änderungen wechseln die Referenz von todos, die zuvor in der Funktion it() erstellt wurden, zu this.todos, das im Mocha-Kontext verfügbar ist. Wir haben auch die Zeilen gelöscht, die zuvor todos in den einzelnen Testfällen instanziierten.

      Führen wir nun diese Datei aus, um zu bestätigen, dass unsere Tests noch funktionieren. Geben Sie npm test in Ihr Terminal ein, um Folgendes zu erhalten:

      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)

      Die Ergebnisse sind gleich, und als zusätzlichen Vorteil haben wir die Einrichtungszeit für neue Tests für die Funktion saveToFile() leicht reduziert sowie eine Lösung für die zurückbleibende CSV-Datei gefunden.

      Zusammenfassung

      In diesem Tutorial haben Sie ein Node.js-Modul geschrieben, um TODO-Elemente zu verwalten und den Code manuell mit der Node.js-REPL getestet. Dann haben Sie eine Testdatei erstellt und das Mocha-Framework zur Ausführung automatisierter Tests verwendet. Mit dem assert-Modul konnten Sie verifizieren, ob Ihr Code funktioniert. Sie haben mit Mocha auch synchrone und asynchrone Funktionen getestet. Schließlich haben Sie Hooks mit Mocha erstellt, die das Schreiben mehrerer verwandter Testfälle wesentlich lesbarer und wartungsfreundlicher machen.

      Mit diesen neuen Kenntnissen können Sie nun versuchen, Tests für neue Node.js-Module zu schreiben, die Sie gerade erstellen. Können Sie über die Ein- und Ausgaben Ihrer Funktion nachdenken und Ihren Test schreiben, bevor Sie Ihren Code erstellen?

      Wenn Sie weitere Informationen über das Mocha-Framework erhalten möchten, besuchen Sie unsere offizielle Mocha-Dokumentation. Wenn Sie gerne noch mehr über Node.js lernen möchten, können Sie zu der Serienseite Codieren in Node.js zurückkehren.



      Source link

      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

      > todos@1.0.0 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