In order to illustrate how to unit test with private functions correctly, I’ll be using the following code that attempts to delete a temporary markdown file.
NOTE: The messaging
module is a simple module that writes standard messages out to the console. I’ve kept all user-facing messages in a single file in order to increase maintainability. The messages aren’t important for this demo and they will be stubbed in our unit tests anyway.
var fs = require('fs'); var path = require('path'); var messaging = require('./messaging'); module.exports = { /** * Deletes the `temppdf.md` file. * * Deletes the temporary PDF markdown file. * * @returns {boolean} Returns true if the file is was successfully deleted (or non-existent). Returns false if the file cannot be deleted (e.g. file lock, etc.). */ deleteTempPdf: async function () { let tempPdf = path.resolve('temppdf.md'); let stat = await checkFileAccess(tempPdf); if (stat == 0) { messaging.printTempPdfMessage(0); return true; } else if (stat == 1) { messaging.printTempPdfMessage(1); return false; } else if (stat == 2) { // File is writable (e.g. no lock), attempt to delete fs.unlinkSync(tempPdf); // Check file status again stat = await checkFileAccess(tempPdf); if (stat == 0) { messaging.printTempPdfMessage(2); return true; } else { messaging.printTempPdfMessage(3); return false; } } else { messaging.printTempPdfMessage(4); return false; } } } /** * Checks file access. * * Checks a given file for access on the filesystem. * * @param {string} file Path of the file. * * @return {number} 0 if file doesn't exist; 1 if file is readonly; 2 if file is writable */ function checkFileAccess(file) { return new Promise((resolve) => { fs.access(file, fs.constants.F_OK | fs.constants.W_OK, (err) => { if (err) { if (err.code === 'ENOENT') { resolve(0); } else { resolve(1); } } else { resolve(2); } }); }); }
What you see here are two methods – one public and one private – deleteTempPdf
and checkFileAccess
. The checkFileAccess
uses the fs
library to check file access conditions.
NOTE: fs.exists
has been deprecated and, therefore, I’m using fs.access
.
As you can see form the JSDoc description, `checkFileAccess` returns one of three possible values: ‘0’ if the file doesn’t exist; ‘1’ if the file is readonly (e.g. possible file lock, or current permissions don’t allow write access); or, ‘2’ if the file does exist and is writable.
Based on these return values, deleteTempPdf
will attempt to perform the necessary operation(s). If the file doesn’t exist or does exist, but is deleted successfully, deleteTempPdf
will return true
for a successful deletion. Otherwise, deleteTempPdf
will return false
, meaning the file cannot be deleted.
Looking at the if-then
structure of deleteTempPdf
the logic is as follows:
- If the file doesn’t exist, return
true
- Else, if the file is readonly, return
false
- Else, if the file does exist, but is writable:
- Attempt to delete the file, then check the file again
- If the file doesn’t exist (deleted successfully), return
true
- Otherwise (the file wasn’t deleted successfully for whatever reason), return
false
- Else (unknown issue), return
false
With these five branches (note that points 3 and 3.1 are simply steps in the process and we only need to test 3.2 and 3.3), we can identify the five unit tests we need to write:
it('should return true for "temppdf.md" not existing')
it('should return false for "temppdf.md" being readonly')
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')
it('should return false for unknown error when checking access of "temppdf.md"')
The initial barebones version of our unit test spec file should look like the following:
describe('tempFile', () => { describe('deleteTempPdf', () => { it('should return true for "temppdf.md" not existing', () => { }) it('should return false for "temppdf.md" being readonly', () => { }) 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', () => { }) it('should return false for unknown error when checking access of "temppdf.md"', () => { }) }) })
Upon successfully writing all five unit tests, we’ll achieve 100% code coverage.
On the next page, we’ll start filling in these unit tests.