Okay, now we’re ready for the super exciting part – tackling those private methods.

Sinon + Rewire

In order to do this, we’re going to use a combination of two test helpers – Sinon and Rewire. I’ll explain what they are for, but first, let’s go ahead and add them to our project:

npm i sinon rewire --save-dev

And, then, of course, add the references to the top of your spec file:

var sinon = require('sinon');
var rewire = require('rewire');

Sinon is a very powerful library for assisting in unit testing. It allows us to write stubs, shims and mocks for our tests. With Sinon we can control the flow of our stubs and shims along with verifying they were called or checking how many times they were called. Sinon has a ton a features and quite too many to cover here. For more info check out their site.

Rewire provides a feature exposed by Node.js called module encapsulation. Rewire encapsulates our module allowing us to “rewire” the cached version and replace the module’s properties in memory. In the end, this is what allows us to rewrite our checkFileAccess function for our tests in memory without having to follow any of the aforementioned AntiPatterns. Finally, we can call rewire on our module unlimited times, and Rewire will create a newly cached version each time.

Setup

In addition to the err variable we added earlier, we’re going to need two more variables – one for a testing sandbox (for stubs) and one for holding the cached version of our module under test (for this example, our module under test in stored in the file ‘tempFile.js’).

Where you added the err variable just inside the parent describe, add two more variables:

describe('tempFile', () => {
	var tempFile;
	var sandbox;
	var err;

...

These two variables will be instantiated in our setup fixtures, which we’ll get to in a minute.

We also need to add a stub for the Node.js fs filesystem module. If you look through the code under test, you’ll see there’s basically three properties of the fs module we need to stub/mock: 1) fs.access; 2) fs.unlinkSync; and, 3) the constants object used by fs.access (namely, the F_OK and W_OK properties).

Just below the global variables you just added, let’s add the necessary stubs for fs:

    var fsStub = {
        constants: {
            F_OK : 0,
            W_OK: 0
        },
        access: function(path, mode, cb) {
            cb(err, []);
        },
        unlinkSync: function(path) { }
    }

Notice a couple of things. First, we don’t necessarily care what the bit values are for the constants because we’re stubbing fs.access. So, we’re just setting both of the constants to ‘0’. Neither fs.access or fs.unlinkSync stubs will do anything. The only additional requirement is for our fs.access stub to call the callback. This callback, you see, is where our err variable from our tests is passed in and evaluated.

Setup and Teardown Fixtures

We’ve taken care of our global variables. Now it’s time to create our fixtures that will run before and after each of our unit tests.

    beforeEach((done) => {
        tempFile = rewire('../tempFile');
        tempFile.__set__({
            'fs': fsStub,
            'messaging': {
                printTempPdfMessage: function() {}
            }
        });

        sandbox = sinon.createSandbox();

        done();
    });

    afterEach((done) => {
        sandbox.restore();

        done();
    });

In the setup fixture (beforeEach), we use Rewire to import an encapsulated version of our code under test (my spec file is in a subdirectory called test, so I reference the file in the parent folder). We use Rewire to import the file instead of the native require statement. Also, as I stated above, each time we rewire the file, a fresh copy is stored in cache. Therefore, we rewire the file before each test to get a clean version.

Next, by using Rewire‘s __set__ method, we overwrite the two default bindings for fs and the referenced messaging.printTempPdfMessage. We replace the native fs binding with our fsStub created above and we replace the messaging.printTempPdfMessage with an empty method. An additional perk to this is that we don’t have to do any creative programming for suppressing our Console.log messages from this method. Instead, the method will still be called from our methods under test; it just won’t do anything (e.g. no console messages will be printed during our tests).

Thirdly, we create a sandbox for Sinon to create the stubs for our tests.

Finally, in the teardown fixture (afterEach) will restore our methods to their original operation for other unit tests by releasing the sandbox.

Final Unit Tests

Now, we’re ready to finish our last two tests. Again, let’s look at them individually.

        it('should return true for "temppdf.md" existing, being writable and being deleted successfully', () => {
            var checkFileAccess = sandbox.stub();
            checkFileAccess.onCall(0).returns(2);
            checkFileAccess.onCall(1).returns(0);

            err = null;

            tempFile.__set__({
                'checkFileAccess': checkFileAccess
            });

            return tempFile.deleteTempPdf().should.eventually.be.true;
        })

The first thing we do is create a stub for our checkFileAccess private method. We’re able to do this because of the sandbox we created in the setup fixture. We’re not going to provide any logic for the method. Instead, we going to explicitly state the return values for the method for each call. You’ll notice that on the first call to the method (0-based index), the method will return the value ‘2’. On the second call, we’re telling Sinon to return the value ‘0’. This allows us to check the logic of our if-then statements in the deletedTempPdf method.

We’re also going to set the err object to null.

Finally, using Rewire‘s __set__ method, along with the fs and messaging.printTempPdfMessage methods above, we overwrite the binding for the private method checkFileAccess with our stub. Again, the method won’t do anything; it just returns the values we’ve specified. This is what’s totally cool about Rewirewe cam overwrite private methods with stubs.

The last unit test is just like this.

        it('should return false for unknown error when checking access of "temppdf.md"', () => {
            var checkFileAccess = sandbox.stub();
            checkFileAccess.onCall(0).returns(3);

            tempFile.__set__({
                'checkFileAccess': checkFileAccess
            });

            return tempFile.deleteTempPdf().should.eventually.be.false;
        })

The only difference in this unit test is we will check how our deleteTempPdf method responds to checkFileAccess returning a value other than 0, 1 or 2. This will force the deleteTempPdf method to enter the final else clause.

And, now you have it! You now have 100% code coverage in your module under test.

Here’s what your final spec file should look like:

var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised).should();
var sinon = require('sinon');
var rewire = require('rewire');


describe('tempFile', () => {
    var tempFile;
    var sandbox;
    var err;

    var fsStub = {
        constants: {
            F_OK : 0,
            W_OK: 0
        },
        access: function(path, mode, cb) {
            cb(err, []);
        },
        unlinkSync: function(path) { }
    }

    beforeEach((done) => {
        tempFile = rewire('../tempFile');
        tempFile.__set__({
            'fs': fsStub,
            'messaging': {
                printTempPdfMessage: function() {}
            }
        });

        sandbox = sinon.createSandbox();

        done();
    });

    afterEach((done) => {
        sandbox.restore();

        done();
    });

    describe('deleteTempPdf', () => {
        it('should return true for "temppdf.md" not existing', () => {
            err = { 
                code: 'ENOENT'
            };

            return tempFile.deleteTempPdf().should.eventually.be.true;
        })

        it('should return false for "temppdf.md" being readonly', () => {
            err = {
                code: 'SOMETHING_ELSE'
            };

            return tempFile.deleteTempPdf().should.eventually.be.false;
        })

        it('should return true for "temppdf.md" existing, being writable and being deleted successfully', () => {
            var checkFileAccess = sandbox.stub();
            checkFileAccess.onCall(0).returns(2);
            checkFileAccess.onCall(1).returns(0);

            err = null;

            tempFile.__set__({
                'checkFileAccess': checkFileAccess
            });

            return tempFile.deleteTempPdf().should.eventually.be.true;
        })

        it('should return false for "temppdf.md" existing, being writable, but not deleted successfully', () => {
            err = null;

            return tempFile.deleteTempPdf().should.eventually.be.false;
        })

        it('should return false for unknown error when checking access of "temppdf.md"', () => {
            var checkFileAccess = sandbox.stub();
            checkFileAccess.onCall(0).returns(3);

            tempFile.__set__({
                'checkFileAccess': checkFileAccess
            });

            return tempFile.deleteTempPdf().should.eventually.be.false;
        })
    })
})

Like What You See?

Subscribe to receive new posts in your inbox.

Privacy Preference Center