One place for hosting & domains

      Module

      How To Test a Node.js Module with Mocha and Assert


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Testing is an integral part of software development. It’s common for programmers to run code that tests their application as they make changes in order to confirm it’s behaving as they’d like. With the right test setup, this process can even be automated, saving a lot of time. Running tests consistently after writing new code ensures that new changes don’t break pre-existing features. This gives the developer confidence in their code base, especially when it gets deployed to production so users can interact with it.

      A test framework structures the way we create test cases. Mocha is a popular JavaScript test framework that organizes our test cases and runs them for us. However, Mocha does not verify our code’s behavior. To compare values in a test, we can use the Node.js assert module.

      In this article, you’ll write tests for a Node.js TODO list module. You will set up and use the Mocha test framework to structure your tests. Then you’ll use the Node.js assert module to create the tests themselves. In this sense, you will be using Mocha as a plan builder, and assert to implement the plan.

      Prerequisites

      Step 1 — Writing a Node Module

      Let’s begin this article by writing the Node.js module we’d like to test. This module will manage a list of TODO items. Using this module, we will be able to list all the TODOs that we are keeping track of, add new items, and mark some as complete. Additionally, we’ll be able to export a list of TODO items to a CSV file. If you’d like a refresher on writing Node.js modules, you can read our article on How To Create a Node.js Module.

      First, we need to set up the coding environment. Create a folder with the name of your project in your terminal. This tutorial will use the name todos:

      Then enter that folder:

      Now initialize npm, since we’ll be using its CLI functionality to run the tests later:

      We only have one dependency, Mocha, which we will use to organize and run our tests. To download and install Mocha, use the following:

      • npm i request --save-dev mocha

      We install Mocha as a dev dependency, as it’s not required by the module in a production setting. If you would like to learn more about Node.js packages or npm, check out our guide on How To Use Node.js Modules with npm and package.json.

      Finally, let’s create our file that will contain our module’s code:

      With that, we’re ready to create our module. Open index.js in a text editor like nano:

      Let’s begin by defining the Todos class. This class contains all the functions that we need to manage our TODO list. Add the following lines of code to index.js:

      todos/index.js

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

      We begin the file by creating a Todos class. Its constructor() function takes no arguments, therefore we don’t need to provide any values to instantiate an object for this class. All we do when we initialize a Todos object is create a todos property that’s an empty array.

      The modules line allows other Node.js modules to require our Todos class. Without explicitly exporting the class, the test file that we will create later would not be able to use it.

      Let’s add a function to return the array of todos we have stored. Write in the following highlighted lines:

      todos/index.js

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

      Our list() function returns a copy of the array that’s used by the class. It makes a copy of the array by using JavaScript’s destructuring syntax. We make a copy of the array so that changes the user makes to the array returned by list() does not affect the array used by the Todos object.

      Note: JavaScript arrays are reference types. This means that for any variable assignment to an array or function invocation with an array as a parameter, JavaScript refers to the original array that was created. For example, if we have an array with three items called x, and create a new variable y such that y = x, y and x both refer to the same thing. Any changes we make to the array with y impacts variable x and vice versa.

      Now let’s write the add() function, which adds a new TODO item:

      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;
      

      Our add() function takes a string, and places it in a new JavaScript object’s title property. The new object also has a completed property, which is set to false by default. We then add this new object to our array of TODOs.

      Important functionality in a TODO manager is to mark items as completed. For this implementation, we will loop through our todos array to find the TODO item the user is searching for. If one is found, we’ll mark it as completed. If none is found, we’ll throw an error.

      Add the complete() function like this:

      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;
      

      Save the file and exit from the text editor.

      We now have a basic TODO manager that we can experiment with. Next, let’s manually test our code to see if the application is working.

      Step 2 — Manually Testing the Code

      In this step, we will run our code’s functions and observe the output to ensure it matches our expectations. This is called manual testing. It’s likely the most common testing methodology programmers apply. Although we will automate our testing later with Mocha, we will first manually test our code to give a better sense of how manual testing differs from testing frameworks.

      Let’s add two TODO items to our app and mark one as complete. Start the Node.js REPL in the same folder as the index.js file:

      You will see the > prompt in the REPL that tells us we can enter JavaScript code. Type the following at the prompt:

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

      With require(), we load the TODOs module into a Todos variable. Recall that our module returns the Todos class by default.

      Now, let’s instantiate an object for that class. In the REPL, add this line of code:

      • const todos = new Todos();

      We can use the todos object to verify our implementation works. Let’s add our first TODO item:

      So far we have not seen any output in our terminal. Let’s verify that we’ve stored our "run code" TODO item by getting a list of all our TODOs:

      You will see this output in your REPL:

      Output

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

      This is the expected result: We have one TODO item in our array of TODOs, and it’s not completed by default.

      Let’s add another TODO item:

      • todos.add("test everything");

      Mark the first TODO item as completed:

      • todos.complete("run code");

      Our todos object will now be managing two items: "run code" and "test everything". The "run code" TODO will be completed as well. Let’s confirm this by calling list() once again:

      The REPL will output:

      Output

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

      Now, exit the REPL with the following:

      We’ve confirmed that our module behaves as we expect it to. While we didn’t put our code in a test file or use a testing library, we did test our code manually. Unfortunately, this form of testing becomes time consuming to do every time we make a change. Next, let’s use automated testing in Node.js and see if we can solve this problem with the Mocha testing framework.

      Step 3 — Writing Your First Test with Mocha and Assert

      In the last step, we manually tested our application. This will work for individual use cases, but as our module scales, this method becomes less viable. As we test new features, we must be certain that the added functionality has not created problems in the old functionality. We would like to test each feature over again for every change in the code, but doing this by hand would take a lot of effort and would be prone to error.

      A more efficient practice would be to set up automated tests. These are scripted tests written like any other code block. We run our functions with defined inputs and inspect their effects to ensure they behave as we expect. As our codebase grows, so will our automated tests. When we write new tests alongside the features, we can verify the entire module still works—all without having to remember how to use each function every time.

      In this tutorial, we’re using the Mocha testing framework with the Node.js assert module. Let’s get some hands-on experience to see how they work together.

      To begin, create a new file to store our test code:

      Now use your preferred text editor to open the test file. You can use nano like before:

      In the first line of the text file, we will load the TODOs module like we did in the Node.js shell. We will then load the assert module for when we write our tests. Add the following lines:

      todos/index.test.js

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

      The strict property of the assert module will allow us to use special equality tests that are recommended by Node.js and are good for future-proofing, since they account for more use cases.

      Before we go into writing tests, let’s discuss how Mocha organizes our code. Tests structured in Mocha usually follow this template:

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

      Notice two key functions: describe() and it(). The describe() function is used to group similar tests. It’s not required for Mocha to run tests, but grouping tests make our test code easier to maintain. It’s recommended that you group your tests in a way that’s easy for you to update similar ones together.

      The it() contains our test code. This is where we would interact with our module’s functions and use the assert library. Many it() functions can be defined in a describe() function.

      Our goal in this section is to use Mocha and assert to automate our manual test. We’ll do this step-by-step, beginning with our describe block. Add the following to your file after the module lines:

      todos/index.test.js

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

      With this code block, we’ve created a grouping for our integrated tests. Unit tests would test one function at a time. Integration tests verify how well functions within or across modules work together. When Mocha runs our test, all the tests within that describe block will run under the "integration test" group.

      Let’s add an it() function so we can begin testing our module’s code:

      todos/index.test.js

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

      Notice how descriptive we made the test’s name. If anyone runs our test, it will be immediately clear what’s passing or failing. A well-tested application is typically a well-documented application, and tests can sometimes be an effective kind of documentation.

      For our first test, we will create a new Todos object and verify it has no items in it:

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

      The first new line of code instantiated a new Todos object as we would do in the Node.js REPL or another module. In the second new line, we use the assert module.

      From the assert module we use the notStrictEqual() method. This function takes two parameters: the value that we want to test (called the actual value) and the value we expect to get (called the expected value). If both arguments are the same, notStrictEqual() throws an error to fail the test.

      Save and exit from index.test.js.

      The base case will be true as the length should be 0, which isn’t 1. Let’s confirm this by running Mocha. To do this, we need to modify our package.json file. Open your package.json file with your text editor:

      Now, in your scripts property, change it so it looks like this:

      todos/package.json

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

      We have just changed the behavior of npm’s CLI test command. When we run npm test, npm will review the command we just entered in package.json. It will look for the Mocha library in our node_modules folder and run the mocha command with our test file.

      Save and exit package.json.

      Let’s see what happens when we run our test. In your terminal, enter:

      The command will produce the following output:

      Output

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

      This output first shows us which group of tests it is about to run. For every individual test within a group, the test case is indented. We see our test name as we described it in the it() function. The tick at the left side of the test case indicates that the test passed.

      At the bottom, we get a summary of all our tests. In our case, our one test is passing and was completed in 16ms (the time varies from computer to computer).

      Our testing has started with success. However, this current test case can allow for false-positives. A false-positive is a test case that passes when it should fail.

      We currently check that the length of the array is not equal to 1. Let’s modify the test so that this condition holds true when it should not. Add the following lines to 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);
          });
      });
      

      Save and exit the file.

      We added two TODO items. Let’s run the test to see what happens:

      This will give the following:

      Output

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

      This passes as expected, as the length is greater than 1. However, it defeats the original purpose of having that first test. The first test is meant to confirm that we start on a blank state. A better test will confirm that in all cases.

      Let’s change the test so it only passes if we have absolutely no TODOs in store. Make the following changes to 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);
          });
      });
      

      You changed notStrictEqual() to strictEqual(), a function that checks for equality between its actual and expected argument. Strict equal will fail if our arguments are not exactly the same.

      Save and exit, then run the test so we can see what happens:

      This time, the output will show an 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.

      This text will be useful for us to debug why the test failed. Notice that since the test failed there was no tick at the beginning of the test case.

      Our test summary is no longer at the bottom of the output, but right after our list of test cases were displayed:

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

      The remaining output provides us with data about our failing tests. First, we see what test case has failed:

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

      Then, we see why our test failed:

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

      An AssertionError is thrown when strictEqual() fails. We see that the expected value, 0, is different from the actual value, 2.

      We then see the line in our test file where the code fails. In this case, it’s line 10.

      Now, we’ve seen for ourselves that our test will fail if we expect incorrect values. Let’s change our test case back to its right value. First, open the file:

      Then take out the todos.add lines so that your code looks like the following:

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

      Save and exit the file.

      Run it once more to confirm that it passes without any potential false-positives:

      The output will be as follows:

      Output

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

      We’ve now improved our test’s resiliency quite a bit. Let’s move forward with our integration test. The next step is to add a new TODO item to 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}]);
          });
      });
      

      After using the add() function, we confirm that we now have one TODO being managed by our todos object with strictEqual(). Our next test confirms the data in the todos with deepStrictEqual(). The deepStrictEqual() function recursively tests that our expected and actual objects have the same properties. In this case, it tests that the arrays we expect both have a JavaScript object within them. It then checks that their JavaScript objects have the same properties, that is, that both their title properties are "run code" and both their completed properties are false.

      We then complete the remaining tests using these two equality checks as needed by adding the following highlighted lines:

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

      Save and exit the file.

      Our test now mimics our manual test. With these programmatic tests, we don’t need to check the output continuously if our tests pass when we run them. You typically want to test every aspect of use to make sure the code is tested properly.

      Let’s run our test with npm test once more to get this familiar output:

      Output

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

      You’ve now set up an integrated test with the Mocha framework and the assert library.

      Let’s consider a situation where we’ve shared our module with some other developers and they’re now giving us feedback. A good portion of our users would like the complete() function to return an error if no TODOs were added as of yet. Let’s add this functionality in our complete() function.

      Open index.js in your text editor:

      Add the following to the function:

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

      Save and exit the file.

      Now let’s add a new test for this new feature. We want to verify that if we call complete on a Todos object that has no items, it will return our special error.

      Go back into index.test.js:

      At the end of the file, add the following code:

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

      We use describe() and it() like before. Our test begins with creating a new todos object. We then define the error we are expecting to receive when we call the complete() function.

      Next, we use the throws() function of the assert module. This function was created so we can verify the errors that are thrown in our code. Its first argument is a function that contains the code that throws the error. The second argument is the error we are expecting to receive.

      In your terminal, run the tests with npm test once again and you will now see the following output:

      Output

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

      This output highlights the benefit of why we do automated testing with Mocha and assert. Because our tests are scripted, every time we run npm test, we verify that all our tests are passing. We did not need to manually check if the other code is still working; we know that it is because the test we have still passed.

      So far, our tests have verified the results of synchronous code. Let’s see how we would need to adapt our newfound testing habits to work with asynchronous code.

      Step 4 — Testing Asynchronous Code

      One of the features we want in our TODO module is a CSV export feature. This will print all the TODOs we have in store along with the completed status to a file. This requires that we use the fs module—a built-in Node.js module for working with the file system.

      Writing to a file is an asynchronous operation. There are many ways to write to a file in Node.js. We can use callbacks, Promises, or the async/await keywords. In this section, we’ll look at how we write tests for those different methods.

      Callbacks

      A callback function is one used as an argument to an asynchronous function. It is called when the asynchronous operation is completed.

      Let’s add a function to our Todos class called saveToFile(). This function will build a string by looping through all our TODO items and writing that string to a file.

      Open your index.js file:

      In this file, add the following highlighted code:

      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;
      

      We first have to import the fs module in our file. Then we added our new saveToFile() function. Our function takes a callback function that will be used once the file write operation is complete. In that function, we create a fileContents variable that stores the entire string we want to be saved as a file. It’s initialized with the CSV’s headers. We then loop through each TODO item with the internal array’s forEach() method. As we iterate, we add the title and completed properties of the individual todos objects.

      Finally, we use the fs module to write the file with the writeFile() function. Our first argument is the file name: todos.csv. The second is the contents of the file, in this case, our fileContents variable. Our last argument is our callback function, which handles any file writing errors.

      Save and exit the file.

      Let’s now write a test for our saveToFile() function. Our test will do two things: confirm that the file exists in the first place, and then verify that it has the right contents.

      Open the index.test.js file:

      let’s begin by loading the fs module at the top of the file, as we’ll use it to help test our results:

      todos/index.test.js

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

      Now, at the end of the file let’s add our new test case:

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

      Like before, we use describe() to group our test separately from the others as it involves new functionality. The it() function is slightly different from our other ones. Usually, the callback function we use has no arguments. This time, we have done as an argument. We need this argument when testing functions with callbacks. The done() callback function is used by Mocha to tell it when an asynchronous function is completed.

      All callback functions being tested in Mocha must call the done() callback. If not, Mocha would never know when the function was complete and would be stuck waiting for a signal.

      Continuing, we create our Todos instance and add a single item to it. We then call the saveToFile() function, with a callback that captures a file writing error. Note how our test for this function resides in the callback. If our test code was outside the callback, it would fail as long as the code was called before the file writing completed.

      In our callback function, we first check that our file exists:

      todos/index.test.js

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

      The fs.existsSync() function returns true if the file path in its argument exists, false otherwise.

      Note: The fs module’s functions are asynchronous by default. However, for key functions, they made synchronous counterparts. This test is simpler by using synchronous functions, as we don’t have to nest the asynchronous code to ensure it works. In the fs module, synchronous functions usually end with "Sync" at the end of their names.

      We then create a variable to store our expected value:

      todos/index.test.js

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

      We use readFileSync() of the fs module to read the file synchronously:

      todos/index.test.js

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

      We now provide readFileSync() with the right path for the file: todos.csv. As readFileSync() returns a Buffer object, which stores binary data, we use its toString() method so we can compare its value with the string we expect to have saved.

      Like before, we use the assert module’s strictEqual to do a comparison:

      todos/index.test.js

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

      We end our test by calling the done() callback, ensuring that Mocha knows to stop testing that case:

      todos/index.test.js

      ...
      done(err);
      ...
      

      We provide the err object to done() so Mocha can fail the test in the case an error occurred.

      Save and exit from index.test.js.

      Let’s run this test with npm test like before. Your console will display this output:

      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)

      You’ve now tested your first asynchronous function with Mocha using callbacks. But at the time of writing this tutorial, Promises are more prevalent than callbacks in new Node.js code, as explained in our How To Write Asynchronous Code in Node.js article. Next, let’s learn how we can test them with Mocha as well.

      Promises

      A Promise is a JavaScript object that will eventually return a value. When a Promise is successful, it is resolved. When it encounters an error, it is rejected.

      Let’s modify the saveToFile() function so that it uses Promises instead of callbacks. Open up index.js:

      First, we need to change how the fs module is loaded. In your index.js file, change the require() statement at the top of the file to look like this:

      todos/index.js

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

      We just imported the fs module that uses Promises instead of callbacks. Now, we need to make some changes to saveToFile() so that it works with Promises instead.

      In your text editor, make the following changes to the saveToFile() function to remove the 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);
      }
      ...
      

      The first difference is that our function no longer accepts any arguments. With Promises we don’t need a callback function. The second change concerns how the file is written. We now return the result of the writeFile() promise.

      Save and close out of index.js.

      Let’s now adapt our test so that it works with Promises. Open up index.test.js:

      Change the saveToFile() test to this:

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

      The first change we need to make is to remove the done() callback from its arguments. If Mocha passes the done() argument, it needs to be called or it will throw an error like this:

      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)
      

      When testing Promises, do not include the done() callback in it().

      To test our promise, we need to put our assertion code in the then() function. Notice that we return this promise in the test, and we don’t have a catch() function to catch when the Promise is rejected.

      We return the promise so that any errors that are thrown in the then() function are bubbled up to the it() function. If the errors are not bubbled up, Mocha will not fail the test case. When testing Promises, you need to use return on the Promise being tested. If not, you run the risk of getting a false-positive.

      We also omit the catch() clause because Mocha can detect when a promise is rejected. If rejected, it automatically fails the test.

      Now that we have our test in place, save and exit the file, then run Mocha with npm test and to confirm we get a successful result:

      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)

      We’ve changed our code and test to use Promises, and now we know for sure that it works. But the most recent asynchronous patterns use async/await keywords so we don’t have to create multiple then() functions to handle successful results. Let’s see how we can test with async/await.

      async/await

      The async/await keywords make working with Promises less verbose. Once we define a function as asynchronous with the async keyword, we can get any future results in that function with the await keyword. This way we can use Promises without having to use the then() or catch() functions.

      We can simplify our saveToFile() test that’s promise based with async/await. In your text editor, make these minor edits to the saveToFile() test in 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);
          });
      });
      

      The first change is that the function used by the it() function now has the async keyword when it’s defined. This allows us to the use the await keyword inside its body.

      The second change is found when we call saveToFile(). The await keyword is used before it is called. Now Node.js knows to wait until this function is resolved before continuing the test.

      Our function code is easier to read now that we moved the code that was in the then() function to the it() function’s body. Running this code with npm test produces this output:

      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)

      We can now test asynchronous functions using any of three asynchronous paradigms appropriately.

      We have covered a lot of ground with testing synchronous and asynchronous code with Mocha. Next, let’s dive in a bit deeper to some other functionality that Mocha offers to improve our testing experience, particularly how hooks can change test environments.

      Step 5 — Using Hooks to Improve Test Cases

      Hooks are a useful feature of Mocha that allows us to configure the environment before and after a test. We typically add hooks within a describe() function block, as they contain setup and teardown logic specific to some test cases.

      Mocha provides four hooks that we can use in our tests:

      • before: This hook is run once before the first test begins.
      • beforeEach: This hook is run before every test case.
      • after: This hook is run once after the last test case is complete.
      • afterEach: This hook is run after every test case.

      When we test a function or feature multiple times, hooks come in handy as they allow us to separate the test’s setup code (like creating the todos object) from the test’s assertion code.

      To see the value of hooks, let’s add more tests to our saveToFile() test block.

      While we have confirmed that we can save our TODO items to a file, we only saved one item. Furthermore, the item was not marked as completed. Let’s add more tests to be sure that the various aspects of our module works.

      First, let’s add a second test to confirm that our file is saved correctly when we have a completed a TODO item. Open your index.test.js file in your text editor:

      Change the last test to the following:

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

      The test is similar to what we had before. The key differences are that we call the complete() function before we call saveToFile(), and that our expectedFileContents now have true instead of false for the completed column’s value.

      Save and exit the file.

      Let’s run our new test, and all the others, with npm test:

      This will give the following:

      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)

      It works as expected. There is, however, room for improvement. They both have to instantiate a Todos object at the beginning of the test. As we add more test cases, this quickly becomes repetitive and memory-wasteful. Also, each time we run the test, it creates a file. This can be mistaken for real output by someone less familiar with the module. It would be nice if we cleaned up our output files after testing.

      Let’s make these improvements using test hooks. We’ll use the beforeEach() hook to set up our test fixture of TODO items. A test fixture is any consistent state used in a test. In our case, our test fixture is a new todos object that has one TODO item added to it already. We will then use afterEach() to remove the file created by the test.

      In index.test.js, make the following changes to your last test for 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);
          });
      });
      

      Let’s break down all the changes we’ve made. We added a beforeEach() block to the test block:

      todos/index.test.js

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

      These two lines of code create a new Todos object that will be available in each of our tests. With Mocha, the this object in beforeEach() refers to the same this object in it(). this is the same for every code block inside the describe() block. For more information on this, see our tutorial Understanding This, Bind, Call, and Apply in JavaScript.

      This powerful context sharing is why we can quickly create test fixtures that work for both of our tests.

      We then clean up our CSV file in the afterEach() function:

      todos/index.test.js

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

      If our test failed, then it may not have created a file. That’s why we check if the file exists before we use the unlinkSync() function to delete it.

      The remaining changes switch the reference from todos, which were previously created in the it() function, to this.todos which is available in the Mocha context. We also deleted the lines that previously instantiated todos in the individual test cases.

      Now, let’s run this file to confirm our tests still work. Enter npm test in your terminal to get:

      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)

      The results are the same, and as a benefit, we have slightly reduced the setup time for new tests for the saveToFile() function and found a solution to the residual CSV file.

      Conclusion

      In this tutorial, you wrote a Node.js module to manage TODO items and tested the code manually using the Node.js REPL. You then created a test file and used the Mocha framework to run automated tests. With the assert module, you were able to verify that your code works. You also tested synchronous and asynchronous functions with Mocha. Finally, you created hooks with Mocha that make writing multiple related test cases much more readable and maintainable.

      Equipped with this understanding, challenge yourself to write tests for new Node.js modules that you are creating. Can you think about the inputs and outputs of your function and write your test before you write your code?

      If you would like more information about the Mocha testing framework, check out the official Mocha documentation. If you’d like to continue learning Node.js, you can return to the How To Code in Node.js series page.



      Source link

      How To Create a Node.js Module


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      In Node.js, a module is a collection of JavaScript functions and objects that can be used by external applications. Describing a piece of code as a module refers less to what the code is and more to what it does—any Node.js file or collection of files can be considered a module if its functions and data are made usable to external programs.

      Because modules provide units of functionality that can be reused in many larger programs, they enable you to create loosely coupled applications that scale with complexity, and open the door for you to share your code with other developers. Being able to write modules that export useful functions and data will allow you to contribute to the wider Node.js community—in fact, all packages that you use on npm were bundled and shared as modules. This makes creating modules an essential skill for a Node.js developer.

      In this tutorial, you will create a Node.js module that suggests what color web developers should use in their designs. You will develop the module by storing the colors as an array, and providing a function to retrieve one randomly. Afterwards, you will run through various ways of importing a module into a Node.js application.

      Prerequisites

      Step 1 — Creating a Module

      This step will guide you through creating your first Node.js module. Your module will contain a collection of colors in an array and provide a function to get one at random. You will use the Node.js built-in exports property to make the function and array available to external programs.

      First, you’ll begin by deciding what data about colors you will store in your module. Every color will be an object that contains a name property that humans can easily identify, and a code property that is a string containing an HTML color code. HTML color codes are six-digit hexadecimal numbers that allow you to change the color of elements on a web page. You can learn more about HTML color codes by reading this HTML Color Codes and Names article.

      You will then decide what colors you want to support in your module. Your module will contain an array called allColors that will contain six colors. Your module will also include a function called getRandomColor() that will randomly select a color from your array and return it.

      In your terminal, make a new folder called colors and move into it:

      Initialize npm so other programs can import this module later in the tutorial:

      You used the -y flag to skip the usual prompts to customize your package.json. If this were a module you wished to publish to npm, you would answer all these prompts with relevant data, as explained in How To Use Node.js Modules with npm and package.json.

      In this case, your output will be:

      Output

      { "name": "colors", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

      Now, open up a command-line text editor such as nano and create a new file to serve as the entry point for your module:

      Your module will do a few things. First, you’ll define a Color class. Your Color class will be instantiated with its name and HTML code. Add the following lines to create the class:

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      

      Now that you have your data structure for Color, add some instances into your module. Write the following highlighted array to your file:

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      
      const allColors = [
        new Color('brightred', '#E74C3C'),
        new Color('soothingpurple', '#9B59B6'),
        new Color('skyblue', '#5DADE2'),
        new Color('leafygreen', '#48C9B0'),
        new Color('sunkissedyellow', '#F4D03F'),
        new Color('groovygray', '#D7DBDD'),
      ];
      

      Finally, enter a function that randomly selects an item from the allColors array you just created:

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      
      const allColors = [
        new Color('brightred', '#E74C3C'),
        new Color('soothingpurple', '#9B59B6'),
        new Color('skyblue', '#5DADE2'),
        new Color('leafygreen', '#48C9B0'),
        new Color('sunkissedyellow', '#F4D03F'),
        new Color('groovygray', '#D7DBDD'),
      ];
      
      exports.getRandomColor = () => {
        return allColors[Math.floor(Math.random() * allColors.length)];
      }
      
      exports.allColors = allColors;
      

      The exports keyword references a global object available in every Node.js module. All functions and objects stored in a module’s exports object are exposed when other Node.js modules import it. The getRandomColor() function was created directly on the exports object, for example. You then added an allColors property to the exports object that references the local constant allColors array created earlier in the script.

      When other modules import this module, both allColors and getRandomColor() will be exposed and available for usage.

      Save and exit the file.

      So far, you have created a module that contains an array of colors and a function that returns one randomly. You have also exported the array and function, so that external programs can use them. In the next step, you will use your module in other applications to demonstrate the effects of export.

      Step 2 — Testing your Module with the REPL

      Before you build a complete application, take a moment to confirm that your module is working. In this step, you will use the REPL to load the colors module. While in the REPL, you will call the getRandomColor() function to see if it behaves as you expect it to.

      Start the Node.js REPL in the same folder as the index.js file:

      When the REPL has started, you will see the > prompt. This means you can enter JavaScript code that will be immediately evaluated. If you would like to read more about this, follow our guide on using the REPL.

      First, enter the following:

      • colors = require('./index');

      In this command, require() loads the colors module at its entry point. When you press ENTER you will get:

      Output

      { getRandomColor: [Function], allColors: [ Color { name: 'brightred', code: '#E74C3C' }, Color { name: 'soothingpurple', code: '#9B59B6' }, Color { name: 'skyblue', code: '#5DADE2' }, Color { name: 'leafygreen', code: '#48C9B0' }, Color { name: 'sunkissedyellow', code: '#F4D03F' }, Color { name: 'groovygray', code: '#D7DBDD' } ] }

      The REPL shows us the value of colors, which are all the functions and objects imported from the index.js file. When you use the require keyword, Node.js returns all the contents within the exports object of a module.

      Recall that you added getRandomColor() and allColors to exports in the colors module. For that reason, you see them both in the REPL when they are imported.

      At the prompt, test the getRandomColor() function:

      You’ll be prompted with a random color:

      Output

      Color { name: 'groovygray', code: '#D7DBDD' }

      As the index is random, your output may vary. Now that you confirmed that the colors module is working, exit the Node.js REPL:

      This will return you to your terminal command line.

      You have just confirmed that your module works as expected using the REPL. Next, you will apply these same concepts and load your module into an application, as you would do in a real project.

      Step 3 — Saving your Local Module as a Dependency

      While testing your module in the REPL, you imported it with a relative path. This means you used the location of the index.js file in relation to the working directory to get its contents. While this works, it is usually a better programming experience to import modules by their names so that the import is not broken when the context is changed. In this step, you will install the colors module with npm’s local module install feature.

      Set up a new Node.js module outside the colors folder. First, go to the previous directory and create a new folder:

      • cd ..
      • mkdir really-large-application

      Now move into your new project:

      • cd really-large-application

      Like with the colors module, initialize your folder with npm:

      The following package.json will be generated:

      Output

      { "name": "really-large-application", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

      Now, install your colors module and use the --save flag so it will be recorded in your package.json file:

      • npm install --save ../colors

      You just installed your colors module in the new project. Open the package.json file to see the new local dependency:

      You will find that the following highlighted lines have been added:

      ~/really-large-application/package.json

      {
        "name": "really-large-application",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "test": "echo "Error: no test specified" && exit 1"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "dependencies": {
          "colors": "file:../colors"
        }
      }
      

      Exit the file.

      The colors module was copied to your node_modules directory. Verify it’s there with the following command:

      This will give the following output:

      Output

      colors

      Use your installed local module in this new program. Re-open your text editor and create another JavaScript file:

      Your program will first import the colors module. It will then choose a color at random using the getRandomColor() function provided by the module. Finally, it will print a message to the console that tells the user what color to use.

      Enter the following code in index.js:

      ~/really-large-application/index.js

      const colors = require('colors');
      
      const chosenColor = colors.getRandomColor();
      console.log(`You should use ${chosenColor.name} on your website. It's HTML code is ${chosenColor.code}`);
      

      Save and exit this file.

      Your application will now tell the user a random color option for a website component.

      Run this script with:

      Your output will be similar to:

      Output

      You should use leafygreen on your website. It's HTML code is #48C9B0

      You’ve now successfully installed the colors module and can manage it like any other npm package used in your project. However, if you added more colors and functions to your local colors module, you would have to run npm update in your applications to be able to use the new options. In the next step, you will use the local module colors in another way and get automatic updates when the module code changes.

      Step 4 — Linking a Local Module

      If your local module is in heavy development, continually updating packages can be tedious. An alternative would be to link the modules. Linking a module ensures that any updates to the module are immediately reflected in the applications using it.

      In this step, you will link the colors module to your application. You will also modify the colors module and confirm that its most recent changes work in the application without having to reinstall or upgrade.

      First, uninstall your local module:

      npm links modules by using symbolic links (or symlinks), which are references that point to files or directories in your computer. Linking a module is done in two steps:

      1. Creating a global link to the module. npm creates a symlink between your global node_modules directory and the directory of your module. The global node_modules directory is the location in which all your system-wide npm packages are installed (any package you install with the -g flag).
      2. Create a local link. npm creates a symlink between your local project that’s using the module and the global link of the module.

      First, create the global link by returning to the colors folder and using the link command:

      • cd ../colors
      • sudo npm link

      Once complete, your shell will output:

      Output

      /usr/local/lib/node_modules/colors -> /home/sammy/colors

      You just created a symlink in your node_modules folder to your colors directory.

      Return to the really-large-application folder and link the package:

      • cd ../really-large-application
      • sudo npm link colors

      You will receive output similar to the following:

      Output

      /home/sammy/really-large-application/node_modules/colors -> /usr/local/lib/node_modules/colors -> /home/sammy/colors

      Note: If you would like to type a bit less, you can use ln instead of link. For example, npm ln colors would have worked the exact same way.

      As the output shows, you just created a symlink from your really-large-application’s local node_modules directory to the colors symlink in your global node_modules, which points to the actual directory with the colors module.

      The linking process is complete. Run your file to ensure it still works:

      Your output will be similar to:

      Output

      You should use sunkissedyellow on your website. It's HTML code is #F4D03F

      Your program functionality is intact. Next, test that updates are immediately applied. In your text editor, re-open the index.js file in the colors module:

      • cd ../colors
      • nano index.js

      Now add a function that selects the very best shade of blue that exists. It takes no arguments, and always returns the third item of the allColors array. Add these lines to the end of the file:

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      
      const allColors = [
        new Color('brightred', '#E74C3C'),
        new Color('soothingpurple', '#9B59B6'),
        new Color('skyblue', '#5DADE2'),
        new Color('leafygreen', '#48C9B0'),
        new Color('sunkissedyellow', '#F4D03F'),
        new Color('groovygray', '#D7DBDD'),
      ];
      
      exports.getRandomColor = () => {
              return allColors[Math.floor(Math.random() * allColors.length)];
              }
      
      exports.allColors = allColors;
      
      exports.getBlue = () => {
        return allColors[2];
      }
      

      Save and exit the file, then re-open the index.js file in the really-large-application folder:

      • cd ../really-large-application
      • nano index.js

      Make a call to the newly created getBlue() function, and print a sentence with the color’s properties. Add these statements to the end of the file:

      ~/really-large-application/index.js

      const colors = require('colors');
      
      const chosenColor = colors.getRandomColor();
      console.log(`You should use ${chosenColor.name} on your website. It's HTML code is ${chosenColor.code}`);
      
      const favoriteColor = colors.getBlue();
      console.log(`My favorite color is ${favoriteColor.name}/${favoriteColor.code}, btw`);
      

      Save and exit the file.

      The code now uses the newly create getBlue() function. Execute the file as before:

      You will get output like:

      Output

      You should use brightred on your website. It's HTML code is #E74C3C My favorite color is skyblue/#5DADE2, btw

      Your script was able to use the latest function in your colors module, without having to run npm update. This will make it easier to make changes to this application in development.

      As you write larger and more complex applications, think about how related code can be grouped into modules, and how you want these modules to be set up. If your module is only going to be used by one program, it can stay within the same project and be referenced by a relative path. If your module will later be shared separately or exists in a very different location from the project you are working on now, installing or linking might be more viable. Modules in active development also benefit from the automatic updates of linking. If the module is not under active development, using npm install may be the easier option.

      Conclusion

      In this tutorial, you learned that a Node.js module is a JavaScript file with functions and objects that can be used by other programs. You then created a module and attached your functions and objects to the global exports object to make them available to external programs. Finally, you imported that module into a program, demonstrating how modules come together into larger applications.

      Now that you know how to create modules, think about the type of program you want to write and break it down into various components, keeping each unique set of activities and data in their own modules. The more practice you get writing modules, the better your ability to write quality Node.js programs on your learning journey. To work through an example of a Node.js application that uses modules, see our How To Set Up a Node.js Application for Production on Ubuntu 18.04 tutorial.



      Source link

      How to use the Linode Ansible Module to Deploy Linodes


      Updated by Linode Contributed by Linode

      Ansible is a popular open-source tool that can be used to automate common IT tasks, like cloud provisioning and configuration management. With Ansible’s 2.8 release, you can deploy Linode instances using our latest API (v4). Ansible’s linode_v4 module adds the functionality needed to deploy and manage Linodes via the command line or in your Ansible Playbooks. While the dynamic inventory plugin for Linode helps you source your Ansible inventory directly from the Linode API (v4).

      In this guide you will learn how to:

      • Deploy and manage Linodes using Ansible and the linode_v4 module.
      • Create an Ansible inventory for your Linode infrastructure using the dynamic inventory plugin for Linode.

      Caution

      This guide’s example instructions will create a 1 GB Nanode billable resource on your Linode account. If you do not want to keep using the Nanode that you create, be sure to delete the resource when you have finished the guide.

      If you remove the resource afterward, you will only be billed for the hour(s) that the resources were present on your account.

      Before You Begin

      Note

      Configure Ansible

      The Ansible configuration file is used to adjust Ansible’s default system settings. Ansible will search for a configuration file in the directories listed below, in the order specified, and apply the first configuration values it finds:

      • ANSIBLE_CONFIG environment variable pointing to a configuration file location. If passed, it will override the default Ansible configuration file.
      • ansible.cfg file in the current directory
      • ~/.ansible.cfg in the home directory
      • /etc/ansible/ansible.cfg

      In this section, you will create an Ansible configuration file and add options to disable host key checking, and to whitelist the Linode inventory plugin. The Ansible configuration file will be located in a development directory that you create, however, it could exist in any of the locations listed above. See Ansible’s official documentation for a full list of available configuration settings.

      Caution

      When storing your Ansible configuration file, ensure that its corresponding directory does not have world-writable permissions. This could pose a security risk that allows malicious users to use Ansible to exploit your local system and remote infrastructure. At minimum, the directory should restrict access to particular users and groups. For example, you can create an ansible group, only add privileged users to the ansible group, and update the Ansible configuration file’s directory to have 764 permissions. See the Linux Users and Groups guide for more information on permissions.
      1. In your home directory, create a directory to hold all of your Ansible related files and move into the directory:

        mkdir development && cd development
        
      2. Create the Ansible configuration file, ansible.cfg in the development directory and add the host_key_checking and enable_plugins options.

        ~/development/ansible.cfg
        1
        2
        3
        4
        5
        6
        
        [defaults]
        host_key_checking = False
        VAULT_PASSWORD_FILE = ./vault-pass
        [inventory]
        enable_plugins = linode
              
        • host_key_checking = False will allow Ansible to SSH into hosts without having to accept the remote server’s host key. This will disable host key checking globally.
        • VAULT_PASSWORD_FILE = ./vault-pass is used to specify a Vault password file to use whenever Ansible Vault requires a password. Ansible Vault offers several options for password management. To learn more password management, read Ansible’s Providing Vault Passwords documentation.
        • enable_plugins = linode enables the Linode dynamic inventory plugin.

      Create a Linode Instance

      You can now begin creating Linode instances using Ansible. In this section, you will create an Ansible Playbook that can deploy Linodes.

      Create your Linode Playbook

      1. Ensure you are in the development directory that you created in the Configure Ansible section:

        cd ~/development
        
      2. Using your preferred text editor, create the Create Linode Playbook file and include the following values:

        ~/development/linode_create.yml
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        
        - name: Create Linode
          hosts: localhost
          vars_files:
              - ./group_vars/example_group/vars
          tasks:
          - name: Create a new Linode.
            linode_v4:
              label: "{{ label }}{{ 100 |random }}"
              access_token: "{{ token }}"
              type: g6-nanode-1
              region: us-east
              image: linode/debian9
              root_pass: "{{ password }}"
              authorized_keys: "{{ ssh_keys }}"
              group: example_group
              tags: example_group
              state: present
            register: my_linode
            
        • The Playbook my_linode contains the Create Linode play, which will be executed on hosts: localhost. This means the Ansible playbook will execute on the local system and use it as a vehicle to deploy the remote Linode instances.
        • The vars_files key provides the location of a local file that contains variable values to populate in the play. The value of any variables defined in the vars file will substitute any Jinja template variables used in the Playbook. Jinja template variables are any variables between curly brackets, like: {{ my_var }}.
        • The Create a new Linode task calls the linode_v4 module and provides all required module parameters as arguments, plus additional arguments to configure the Linode’s deployment. For details on each parameter, see the linode_v4 Module Parameters section.

          Note

          Usage of groups is deprecated, but still supported by Linode’s API v4. The Linode dynamic inventory module requires groups to generate an Ansible inventory and will be used later in this guide.
        • Theregister keyword defines a variable name, my_linode that will store linode_v4 module return data. For instance, you could reference the my_linode variable later in your Playbook to complete other actions using data about your Linode. This keyword is not required to deploy a Linode instance, but represents a common way to declare and use variables in Ansible Playbooks. The task in the snippet below will use Ansible’s debug module and the my_linode variable to print out a message with the Linode instance’s ID and IPv4 address during Playbook execution.

          1
          2
          3
          4
          5
          
          ...
          - name: Print info about my Linode instance
              debug:
                msg: "ID is {{ my_linode.instance.id }} IP is {{ my_linode.instance.ipv4 }}"
                  

      Create the Variables File

      In the previous section, you created the Create Linode Playbook to deploy Linode instances and made use of Jinja template variables. In this section, you will create the variables file to provide values to those template variables.

      1. Create the directory to store your Playbook’s variable files. The directory is structured to group your variable files by inventory group. This directory structure supports the use of file level encryption that Ansible Vault can detect and parse. Although it is not relevant to this guide’s example, it will be used as a best practice.

        mkdir -p ~/development/group_vars/example_group/
        
      2. Create the variables file and populate it with the example variables. You can replace the values with your own.

        ~/development/group_vars/example_group/vars
        1
        2
        3
        4
        
        ssh_keys: >
                ['ssh-rsa AAAAB3N..5bYqyRaQ== user@mycomputer', '~/.ssh/id_rsa.pub']
        label: simple-linode-
            
        • The ssh_keys example passes a list of two public SSH keys. The first provides the string value of the key, while the second provides a local public key file location.

          Configure your SSH Agent

          If your SSH Keys are passphrase-protected, you should add the keys to your SSH agent so that Ansible does not hang when running Playbooks on the remote Linode. The following instructions are for Linux systems:

          1. Run the following command; if you stored your private key in another location, update the path that’s passed to ssh-add accordingly:

            eval $(ssh-agent) && ssh-add ~/.ssh/id_rsa
            

            If you start a new terminal, you will need to run the commands in this step again before having access to the keys stored in your SSH agent.

        • label provides a label prefix that will be concatenated with a random number. This occurs when the Create Linode Playbook’s Jinja templating for the label argument is parsed (label: "{{ label }}{{ 100 |random }}").

      Encrypt Sensitive Variables with Ansible Vault

      Ansible Vault allows you to encrypt sensitive data, like passwords or tokens, to keep them from being exposed in your Ansible Playbooks or Roles. You will take advantage of this functionality to keep your Linode instance’s password and access_token encrypted within the variables file.

      Note

      Ansible Vault can also encrypt entire files containing sensitive values. View Ansible’s documentation on Vault for more information.
      1. Create your Ansible Vault password file and add your password to the file. Remember the location of the password file was configured in the ansible.cfg file in the Configure Ansible section of this guide.

        ~/development/vault-pass
        1
        2
        
        My.ANS1BLEvault-c00lPassw0rd
            
      2. Encrypt the value of your Linode’s root user password using Ansible Vault. Replace My.c00lPassw0rd with your own strong password that conforms to the root_pass parameter’s constraints.

        ansible-vault encrypt_string 'My.c00lPassw0rd' --name 'password'
        

        You will see a similar output:

          
              password: !vault |
                  $ANSIBLE_VAULT;1.1;AES256
                  30376134633639613832373335313062366536313334316465303462656664333064373933393831
                  3432313261613532346134633761316363363535326333360a626431376265373133653535373238
                  38323166666665376366663964343830633462623537623065356364343831316439396462343935
                  6233646239363434380a383433643763373066633535366137346638613261353064353466303734
                  3833
        Encryption successful
      3. Copy the generated output and add it to your vars file.

      4. Encrypt the value of your access token. Replace the value of 86210...1e1c6bd with your own access token.

        ansible-vault encrypt_string '86210...1e1c6bd' --name 'token'
        
      5. Copy the generated output and append it to the bottom of your vars file.

        The final vars file should resemble the example below:

        ~/development/group_vars/example_group/vars
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        
        ssh_keys: >
                ['ssh-rsa AAAAB3N..5bYqyRaQ== user@mycomputer', '~/.ssh/id_rsa.pub']
        label: simple-linode-
        password: !vault |
                  $ANSIBLE_VAULT;1.1;AES256
                  30376134633639613832373335313062366536313334316465303462656664333064373933393831
                  3432313261613532346134633761316363363535326333360a626431376265373133653535373238
                  38323166666665376366663964343830633462623537623065356364343831316439396462343935
                  6233646239363434380a383433643763373066633535366137346638613261353064353466303734
                  3833
        token: !vault |
                  $ANSIBLE_VAULT;1.1;AES256
                  65363565316233613963653465613661316134333164623962643834383632646439306566623061
                  3938393939373039373135663239633162336530373738300a316661373731623538306164363434
                  31656434356431353734666633656534343237333662613036653137396235353833313430626534
                  3330323437653835660a303865636365303532373864613632323930343265343665393432326231
                  61313635653463333630636631336539643430326662373137303166303739616262643338373834
                  34613532353031333731336339396233623533326130376431346462633832353432316163373833
                  35316333626530643736636332323161353139306533633961376432623161626132353933373661
                  36663135323664663130
            

      Run the Ansible Playbook

      You are now ready to run the Create Linode Playbook. When you run the Playbook, a 1 GB Nanode will be deployed in the Newark data center. Note: you want to run Ansible commands from the directory where your ansible.cfg file is located.

      1. Run your playbook to create your Linode instances.

        ansible-playbook ~/development/linode_create.yml
        

        You will see a similar output:

        PLAY [Create Linode] *********************************************************************
        
        TASK [Gathering Facts] *******************************************************************
        ok: [localhost]
        
        TASK [Create a new Linode.] **************************************************************
        changed: [localhost]
        
        PLAY RECAP *******************************************************************************
        localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
        

      linode_v4 Module Parameters

      ParameterData type/StatusUsage
      access_tokenstring, requiredYour Linode API v4 access token. The token should have permission to read and write Linodes. The token can also be specified by exposing the LINODE_ACCESS_TOKEN environment variable.
      authorized_keyslistA list of SSH public keys or SSH public key file locations on your local system, for example, ['averylongstring','~/.ssh/id_rsa.pub']. The public key will be stored in the /root/.ssh/authorized_keys file on your Linode. Ansible will use the public key to SSH into your Linodes as the root user and execute your Playbooks.
      groupstring, deprecatedThe Linode instance’s group. Please note, group labelling is deprecated but still supported. The encouraged method for marking instances is to use tags. This parameter must be provided to use the Linode dynamic inventory module.
      imagestringThe Image ID to deploy the Linode disk from. Official Linode Images start with linode/, while your private images start with private/. For example, use linode/ubuntu18.04 to deploy a Linode instance with the Ubuntu 18.04 image. This is a required parameter only when creating Linode instances.

      To view a list of all available Linode images, issue the following command:

      curl https://api.linode.com/v4/images.

      labelstring, requiredThe Linode instance label. The label is used by the module as the main determiner for idempotence and must be a unique value.

      Linode labels have the following constraints:

      • Must start with an alpha character.
      • May only consist of alphanumeric characters, dashes (-), underscores (_) or periods (.).
      • Cannot have two dashes (–), underscores (__) or periods (..) in a row.

      regionstringThe region where the Linode will be located. This is a required parameter only when creating Linode instances.

      To view a list of all available regions, issue the following command:

      curl https://api.linode.com/v4/regions.

      root_passstringThe password for the root user. If not specified, will be generated. This generated password will be available in the task success JSON.

      The root password must conform to the following constraints:

      • May only use alphanumerics, punctuation, spaces, and tabs.
      • Must contain at least two of the following characters classes: upper-case letters, lower-case letters, digits, punctuation.

      statestring, requiredThe desired instance state. The accepted values are absent and present.
      tagslistThe user-defined labels attached to Linodes. Tags are used for grouping Linodes in a way that is relevant to the user.
      typestring,The Linode instance’s plan type. The plan type determines your Linode’s hardware resources and its pricing.

      To view a list of all available Linode types including pricing and specifications for each type, issue the following command:

      curl https://api.linode.com/v4/linode/types.

      The Linode Dynamic Inventory Plugin

      Ansible uses inventories to manage different hosts that make up your infrastructure. This allows you to execute tasks on specific parts of your infrastructure. By default, Ansible will look in /etc/ansible/hosts for an inventory, however, you can designate a different location for your inventory file and use multiple inventory files that represent your infrastructure. To support infrastructures that shift over time, Ansible offers the ability to track inventory from dynamic sources, like cloud providers. The Ansible dynamic inventory plugin for Linode can be used to source your inventory from Linode’s API v4. In this section, you will use the Linode plugin to source your Ansible deployed Linode inventory.

      Note

      The dynamic inventory plugin for Linode was enabled in the Ansible configuration file created in the Configure Ansible section of this guide.

      Configure the Plugin

      1. Configure the Ansible dynamic inventory plugin for Linode by creating a file named linode.yml.

        ~/development/linode.yml
        1
        2
        3
        4
        5
        6
        7
        
        plugin: linode
        regions:
          - us-east
        groups:
          - example_group
        types:
          - g6-nanode-1
        • The configuration file will create an inventory for any Linodes on your account that are in the us-east region, part of the example_group group and of type g6-nanode-1. Any Linodes that are not part of the example_group group, but that fulfill the us-east region and g6-nanode-type type will be displayed as ungrouped. All other Linodes will be excluded from the dynamic inventory. For more information on all supported parameters, see the Plugin Parameters section.

      Run the Inventory Plugin

      1. Export your Linode API v4 access token to the shell environment. LINODE_ACCESS_TOKEN must be used as the environment variable name. Replace mytoken with your own access token.

        export LINODE_ACCESS_TOKEN='mytoken'
        
      2. Run the Linode dynamic inventory plugin.

        ansible-inventory -i ~/development/linode.yml --graph
        

        You should see a similar output. The output may vary depending on the Linodes already deployed to your account and the parameter values you pass.

        @all:
        |--@example_group:
        |  |--simple-linode-29
        

        For a more detailed output including all Linode instance configurations, issue the following command:

        ansible-inventory -i ~/development/linode.yml --graph --vars
        
      3. Before you can communicate with your Linode instances using the dynamic inventory plugin, you will need to add your Linode’s IPv4 address and label to your /etc/hosts file.

        The Linode Dynamic Inventory Plugin assumes that the Linodes in your account have labels that correspond to hostnames that are in your resolver search path, /etc/hosts. This means you will have to create an entry in your /etc/hosts file to map the Linode’s IPv4 address to its hostname.

        Note

        A pull request currently exists to support using a public IP, private IP or hostname. This change will enable the inventory plugin to be used with infrastructure that does not have DNS hostnames or hostnames that match Linode labels.

        To add your deployed Linode instance to the /etc/hosts file:

        • Retrieve your Linode instance’s IPv4 address:

          ansible-inventory -i ~/development/linode.yml --graph --vars | grep 'ipv4|simple-linode'
          

          Your output will resemble the following:

          |  |--simple-linode-36
          |  |  |--{ipv4 = [u'192.0.2.0']}
          |  |  |--{label = simple-linode-36}
          
        • Open the /etc/hosts file and add your Linode’s IPv4 address and label:

          /etc/hosts
          1
          2
          3
          
          127.0.0.1       localhost
          192.0.2.0 simple-linode-29
                    
      4. Verify that you can communicate with your grouped inventory by pinging the Linodes. The ping command will use the dynamic inventory plugin configuration file to target example_group. The u root option will run the command as root on the Linode hosts.

        ansible -m ping example_group -i ~/development/linode.yml -u root
        

        You should see a similar output:

        simple-linode-29 | SUCCESS => {
            "ansible_facts": {
                "discovered_interpreter_python": "/usr/bin/python"
            },
            "changed": false,
            "ping": "pong"
        }
        

      Plugin Parameters

      ParameterData type/StatusUsage
      access_tokenstring, requiredYour Linode API v4 access token. The token should have permission to read and write Linodes. The token can also be specified by exposing the LINODE_ACCESS_TOKEN environment variable.
      pluginstring, requiredThe plugin name. The value must always be linode in order to use the dynamic inventory plugin for Linode.
      regionslistThe Linode region with which to populate the inventory. For example, us-east is possible value for this parameter.

      To view a list of all available Linode images, issue the following command:

      curl https://api.linode.com/v4/images.

      typeslistThe Linode type with which to populate the inventory. For example, g6-nanode-1 is a possible value for this parameter.

      To view a list of all available Linode types including pricing and specifications for each type, issue the following command:

      curl https://api.linode.com/v4/linode/types.

      groupslistThe Linode group with which to populate the inventory. Please note, group labelling is deprecated but still supported. The encouraged method for marking instances is to use tags. This parameter must be provided to use the Linode dynamic inventory module.

      Delete Your Resources

      1. To delete the Linode instance created in this guide, create a Delete Linode Playbook with the following content in the example. Replace the value of label with your Linode’s label:

        ~/development/linode_delete.yml
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        
        - name: Delete Linode
          hosts: localhost
          vars_files:
            - ./group_vars/example_group/vars
          tasks:
          - name: Delete your Linode Instance.
            linode_v4:
              label: simple-linode-29
              state: absent
              
      2. Run the Delete Linode Playbook:

        ansible-playbook ~/development/linode_delete.yml
        

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link