/ NodeJS

NodeJS — A beginners guide to a lot of stuff. Part 3

Check out Part 2

Testing

We left off on the last article just before setting up our tests.
Here is how I setup my test suite (so far). It is not standard as far as I have seen, but I feel I get a better sense of control over how I test my application.

My base test.js file handles most of the setup:

process.env.NODE_ENV = 'test';
const db = require('../src/mysql');
const config = require('config');
try{
  db.connect((err) =>{
    if (err){
      throw Error('Could not connect to database');
    }
  })
}catch(err){
  console.log(err);
  process.exit(1);
}
/**
* Important Stuff for now - starts here
*/
function importTest(desc, file){
  describe(desc, function(){
    require(file);
  });
}
describe('Test Suite using interfaces', function(){
  importTest('Test Category Model','./models/testCategory');
})

I left my test file fleshed out — later on we will use all the stuff of the first few lines, focus on the important stuff at the end of the file.

function importTest(desc, file){
  describe(desc, function(){
    require(file);
  });
}

This function basically imports a testFile and adds it into a describe block. This is very handy and keeps the test files small and focused.

describe('Test Suite using interfaces', function(){
  importTest('Test Category Model','./models/testCategory');
})

This should be self explanatory, but, basically, this is where I load my testFile using my function. I place it inside a describe block to keep everything organized.

Now to my TestCategory file, please notice that I keep my file inside a models directory inside the test folder. This is a similar structure that I have for my App. I like to keep things intuitive, so I reflect my Apps directory structure within my test structure.

const CategoryMock = require('../../src/models/CategoryMock')
const chai = require('chai');
let should = chai.should();
function mockDataSample(){
  let sd = [];
  for (let i=1;i<11;i++){
    sd.push({
      id:i,
      name:'Data ' + i.toString(),
      slug:'data-' + i.toString()
    })
  }
  return sd;
}
var MockCat = new CategoryMock(mockDataSample());
function runCategoryTest(myCategory){
  describe('Test getAll', function(){
    it('should return all records', function(done){
      myCategory.getAll(function(err, results){
        should.not.exist(err);
        should.exist(results);
        results.should.be.a('array');
        done();
      })
    })
  });
  describe('Test get ID', function(){
    it('Should return an object with ID of 2', function(done){
      let obj = myCategory.getId(2, function(err, result){
        result.should.be.a('object');
        result.id.should.be.eql(2);
        done();
      })
    })
  });
}
// RUN THE TEST
describe('Test the mocked Category', function(){
  runCategoryTest(MockCat);
})

Now I know, at first glance, this looks over complicated… let me break it down and you’ll see there is some reason to my insanity.

function mockDataSample(){...}
This is the function responsible for creating our fake data for our Mock Categories. Not pretty, but it works.

var MockCat = new CategoryMock(mockDataSample());
Create an object that will be used on the test, and supply it with our fake data (remember the constructor from our previous article?)
Now the bit that probably is confusing:

function runCategoryTest(myCategory){
  describe('Test getAll', function(){
    it('should return all records', function(done){
      myCategory.getAll(function(err, results){
        should.not.exist(err);
        should.exist(results);
        results.should.be.a('array');
...

This function holds all of the tests we expect to do on our category object, independent of the implementation we choose. It uses the parameter myCategory to inject a specific implementation.
I created this function so that I can, with ease, re-use the tests for multiple implementations. Notice that in my it blocks, I call myCategory.method…

Finally, I run the tests accordingly, injecting my MockCat object.

describe('Test the mocked Category', function(){
  runCategoryTest(MockCat);
})

Fine. Did you see an interesting pattern emerging here? You might have already guessed that something quite interesting has fallen into place.

I know the tests in this file are very simple, but they point out something very interesting. Let’s look at our getAll test in more detail and compare it with other parts of our code. If you look at the code so far, we are querying against the “expected behavior” of our category class, not against the behavior of a specific implementation.

By looking at ICategoryRepository, we know our category object has a method getAll. We know that getAll takes only a callback as a parameter.

By looking at one of the implementations, we see that getAll returns two things to the callback, the first, an error object if we have an error, or a null if everything goes OK, and second, an array of results.

By looking at our tests, we know what to excpect too…

should.not.exist(err);
should.exist(results);
results.should.be.a('array');

No error should exist, we should have a results element, and the result element should be of type array. We could go on and do more assertions, but for this example, I guess this would be enough.

So what has all this given us you might ask. You spent a good hour getting your head around this, testing code, and so far, you have an interface, a mock implementation of the interface, and a test suite for all the mock methods, yet, you have nothing that you can use in production you might be thinking.

You are right, but, wrong at the same time. Let’s assume, that now that you have all the methods and their tests running clean. No unexpected errors (we’ll dive into that later on in my articles), all angles of your tests are covered, your run against istanbul gives you 100% coverage on your implemented model. Where do you go from here?

Well, remember all that boiler plate on our test file I told you not to worry about? Let’s talk about that now?

Continue onto Part 4

Gregory Brown

Gregory Brown

Baker, cake designer, cook and just by chance, a full featured developer that is passionate about technology.

Read More