That was good, but we obviously won't run Notes in production with the in-memory Notes model. This means that we need to test all the other models.
Testing the LevelUP and filesystem models is easy, just add this to the scripts section of package.json:
"test-notes-levelup": "NOTES_MODEL=levelup mocha", "test-notes-fs": "NOTES_MODEL=fs mocha",
Then run the following command:
$ npm run test-notes-fs $ npm run test-notes-levelup
This will produce a successful test result.
The simplest database to test is SQLite3, since it requires zero setup. We have two SQLite3 models to test, let's start with notes-sqlite3.js. Add the following to the scripts section of package.json:
"test-notes-sqlite3": "rm -f chap11.sqlite3 && sqlite3 chap11.sqlite3 --init ../models/chap07.sql </dev/null && NOTES_MODEL=sqlite3 SQLITE_FILE=chap11.sqlite3 mocha test-model",
This command sequence puts the test database in the chap11.sqlite3 file. It first initializes that database using the sqlite3 command-line tool. Note that we've connected its input to /dev/null because the sqlite3 command will prompt for input otherwise. Then, it runs the test suite passing in environment variables required to run against the SQLite3 model.
Running the test suite does find two errors:
$ npm run test-notes-sqlite3
> notes-test@1.0.0 test-notes-sqlite3 /Users/david/chap11/notes/test
> rm -f chap11.sqlite3 && sqlite3 chap11.sqlite3 --init ../models/chap07.sql </dev/null && NOTES_MODEL=sqlite3 SQLITE_FILE=chap11.sqlite3 mocha test-model
Model Test
check keylist
√ should have three entries
√ should have keys n1 n2 n3
√ should have titles Node #
read note
√ should have proper note
1) Unknown note should fail
change note
√ after a successful model.update (114ms)
destroy note
√ should remove note (103ms)
2) should fail to remove unknown note
6 passing (6s)
2 failing
1) Model Test
read note
Unknown note should fail:
Uncaught TypeError: Cannot read property 'notekey' of undefined
at Statement.db.get (/home/david/nodewebdev/node-web-development-
code-4th-edition/chap11/notes/models/notes-sqlite3.mjs:64:39)
2) Model Test
destroy note
should fail to remove unknown note:
AssertionError: expected 'should not get here' to not equal
'should not get here'
+ expected - actual
The failing test calls model.read("badkey12"), a key which we know does not exist. Writing negative tests paid off. The failing line of code at models/notes-sqlite3.mjs (line 64) reads as follows:
const note = new Note(row.notekey, row.title, row.body);
It's easy enough to insert console.log(util.inspect(row)); just before this and learn that, for the failing call, SQLite3 gave us undefined for row, explaining the error message.
The test suite calls the read function multiple times with a notekey value that does exist. Obviously, when given an invalid notekey value, the query gives an empty results set and SQLite3 invokes the callback with both the undefined error and the undefined row values. This is common behavior for database modules. An empty result set isn't an error, and therefore we received no error and an undefined row.
In fact, we saw this behavior earlier with models/notes-sequelize.mjs. The equivalent code in models/notes-sequelize.mjs does the right thing, and it has a check, which we can adapt. Let's rewrite the read function in models/notes-sqlite.mjs to this:
export async function read(key) {
var db = await connectDB();
var note = await new Promise((resolve, reject) => {
db.get("SELECT * FROM notes WHERE notekey = ?", [ key ], (err, row)
=> {
if (err) return reject(err);
if (!row) { reject(new Error(`No note found for ${key}`)); }
else {
const note = new Note(row.notekey, row.title, row.body);
resolve(note);
}
});
});
return note;
}
This is simple, we just check whether row is undefined and, if so, throw an error. While the database doesn't see an empty results set as an error, Notes does. Furthermore, Notes already knows how to deal with a thrown error in this case. Make this change and that particular test case passes.
There is a second similar error in the destroy logic. The test to destroy a nonexistent note fails to produce an error at this line:
await model.destroy("badkey12");
If we inspect the other models, they're throwing errors for a nonexistent key. In SQL, it obviously is not an error if this SQL (from models/notes-sqlite3.mjs) does not delete anything:
db.run("DELETE FROM notes WHERE notekey = ?;", ... );
Unfortunately, there isn't a SQL option to make this SQL statement fail if it does not delete any records. Therefore, we must add a check to see if a record exists. Namely:
export async function destroy(key) {
const db = await connectDB();
const note = await read(key);
return await new Promise((resolve, reject) => {
db.run("DELETE FROM notes WHERE notekey = ?;", [ key ], err =>
{
if (err) return reject(err);
resolve();
});
});
}
Therefore, we read the note and as a byproduct we verify the note exists. If the note doesn't exist, read will throw an error, and the DELETE operation will not even run.
These are the bugs we referred to in Chapter 7, Data Storage and Retrieval. We simply forgot to check for these conditions in this particular model. Thankfully, our diligent testing caught the problem. At least, that's the story to tell the managers rather than telling them that we forgot to check for something we already knew could happen.
Now that we've fixed models/notes-sqlite3.mjs, let's also test models/notes-sequelize.mjs using the SQLite3 database. To do this, we need a connection object to specify in the SEQUELIZE_CONNECT variable. While we can reuse the existing one, let's create a new one. Create a file named test/sequelize-sqlite.yaml containing this:
dbname: notestest
username:
password:
params:
dialect: sqlite
storage: notestest-sequelize.sqlite3
logging: false
This way, we don't overwrite the production database instance with our test suite. Since the test suite destroys the database it tests, it must be run against a database we are comfortable destroying. The logging parameter turns off the voluminous output Sequelize produces so that we can read the test results report.
Add the following to the scripts section of package.json:
"test-notes-sequelize-sqlite": "NOTES_MODEL=sequelize SEQUELIZE_CONNECT=sequelize-sqlite.yaml mocha test-model"
Then run the test suite:
$ npm run test-notes-sequelize-sqlite .. 8 passing (2s)
We pass with flying colors! We've been able to leverage the same test suite against multiple Notes models. We even found two bugs in one model. But, we have two test configurations remaining to test.
Our test results matrix reads as follows:
- models-fs: PASS
- models-memory: PASS
- models-levelup: PASS
- models-sqlite3: 2 failures, now fixed
- models-sequelize: with SQLite3: PASS
- models-sequelize: with MySQL: untested
- models-mongodb: untested
The two untested models both require the setup of a database server. We avoided testing these combinations, but our manager won't accept that excuse because the CEO needs to know we've completed the test cycles. Notes must be tested in a similar configuration to the production environment.
In production, we'll be using a regular database server, of course, with MySQL or MongoDB being the primary choices. Therefore, we need a way that incurs a low overhead to run tests against those databases. Testing against the production configuration must be so easy that we should feel no resistance in doing so, to ensure that tests are run often enough to make the desired impact.
Fortunately, we've already had experience of a technology that supports easily creating and destroying the deployment infrastructure. Hello, Docker!