Initial Prerequisites
For our unit test assertions we’re going to be using Mocha and Chai. Let’s add these to our project:
npm i mocha chai --save-dev
Additionally, you’ll see that our checkFileAccess
private method uses promises. Chai has an additional library to handle promises. Let’s add this library to our project:
npm i chai-as-promised --save-dev
Finally, let’s import these for use in our specs by adding the following to the top of the test file:
var chai = require('chai'); var chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised).should();
One Final Note
There are a number of node modules that will “mock” the filesystem for unit testing against methods using fs
. In essence, they create a physical, temporary file structure. This, in my opinion, is a terrible idea as 1) this is, technically, an integration test; 2) your test runner may execute tests in parallel and create race issues where the file exists and shouldn’t, or vice versa; and, 3) if your test runner errors out in the middle of testing, then the filesystem isn’t cleaned up and will requiring manual cleaning (e.g. deleting temporary files and folders) before the next run. For these reasons, it is my preference to, instead, stub fs
and control the results.
In order to stub fs
, we’re going to need to stub the error code that’s returned by fs.access
. So let’s go ahead and add that variable just inside our top describe
:
var err;
First Three Tests
The first three tests are pretty simple to complete. You’ll see in the code below. However, our tests won’t actually pass yet as we haven’t stubbed fs
or printTempPdfMessage
. We’ll do that in a minute. But, let’s go ahead and add the necessary code to the tests.
We’ll cover each test individually.
it('should return true for "temppdf.md" not existing', () => { err = { code: 'ENOENT' }; return tempFile.deleteTempPdf().should.eventually.be.true; })
In our first test, fs.access
should return an object with the code ENOENT
indicating that the temppdf.md
doesn’t exist. So, we mock the object to ensure that fs.access
returns the correct result. In turn, checkFileAccess
will return ‘0’ and satisfy our first condition. As for the test, the should.eventually.be
assertion is an extension of Chai made available to us through chai-as-promised
. This test will allow us to test our promise in checkFileAccess
. The final note is that tempFile
is referencing the imported module of our methods under test. We’ll import this file later in this post.
it('should return false for "temppdf.md" being readonly', () => { err = { code: 'SOMETHING_ELSE' }; return tempFile.deleteTempPdf().should.eventually.be.false; })
This test is very similar to our first with the exception of the error code. In the first, we’re expecting ENOENT
for indicating that the temppdf.md
file doesn’t exist. However, for this case, we do want the file to exist, but be read-only. In order to test this, we simply need the error code to be anything but ENOENT
. Therefore, we simply provide a random value.
it('should return false for "temppdf.md" existing, being writable, but not deleted successfully', () => { err = null; return tempFile.deleteTempPdf().should.eventually.be.false; })
Our third test will pass in null
for our error. This will force checkFileAccess
to return ‘2’ both times it is called from deleteTempPdf
eventually resulting in a return value of false
.
At this point, we’ve completed three of our five tests. Again, they won’t run yet as we still need to stub a couple of methods, but that’s where the real magic happens and we’ll take a look at that on the next page. As for now, your tests should look like the following:
describe('tempFile', () => { var err; 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', () => { }) 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"', () => { }) }) })
This was an extremely helpful walkthrough – exactly what I needed. Also a very good real world example of something programmers will encounter. The code I’ve been assigned to test is full of situations like this.
Thanks!
Great blog Joshua! Should we also unset the rewired module after each test?
“`
const unset = tempFile.__set__({
‘checkFileAccess’: checkFileAccess
});
…testing…
unset();
“`
Thank you.
Great question! You could unset the module if you’d like, however, it isn’t absolutely necessary for two reasons: 1) before each test, you are reloading the temp file and 2) the temp file that you are using is a cached version maintained by rewire. In other words, you’re not overriding (or stubbing) the actual imported implementation, only a copy of it. In reality, the only time you may wish to unset it is if you set the method for a specific test case, but need to unset it for other cases within the same spec.
Hopefully, this makes sense.
And yet this does not explain how to test private methods. The example shows how to test different return values from checkFileAccess but it does not in fact check that checkFileAccess would return these values in different scenarios. The tests only test the deleteTempPdf function under different condition. But how can you trust the checkFileAccess function if you didn’t have a single test that checks its behavior and that it in fact returns the correct values by doing the fs.access() correctly? You didn’t.
Actually it does. You see, that’s exactly the point of explicitly setting the
err
object. When you set theerr
object, you’re stubbing the return value offs.access()
. We’re not testingfs.access()
. Instead, we’re testingcheckFileAccess()
based on the various values (e.g., error codes) thatfs.access()
could possibly return.