Skip to content

Commit

Permalink
Unit test the config.js module
Browse files Browse the repository at this point in the history
Create a unit test suite for the config.js module. To enable this I
refactored the code to take in a file system interface (which just so
happens to match the `node:fs` interface) to make it possible to test
the logic without having to actually use the file system. Namely, the
test includes some logic to create a mocked in-memory file system that
is used for unit testing.

The rest of the existing code only had to be updated to provide the
`node:fs` module as a dependency to the config.js module.

Signed-off-by: Eric Cornelissen <[email protected]>
  • Loading branch information
ericcornelissen committed Dec 1, 2024
1 parent 4058f9a commit 46caab5
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 5 deletions.
3 changes: 2 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import * as fs from "node:fs/promises";
import { argv, exit } from "node:process";

import { readConfig } from "./config.js";
Expand Down Expand Up @@ -39,7 +40,7 @@ to ignore npm deprecation warnings for your dependencies.

try {
const [config, deprecations] = await Promise.all([
readConfig(),
readConfig(fs),
obtainDeprecations().then(obtainDependencyPaths),
]);

Expand Down
19 changes: 15 additions & 4 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import { readFile } from "node:fs/promises";

export async function readConfig() {
/**
* @param {FileSystem} fs
* @returns {Object}
*/
export async function readConfig(fs) {
let rawConfig;
try {
rawConfig = await readFile("./.ndmrc", { encoding: "utf-8" });
rawConfig = await fs.readFile("./.ndmrc");
} catch (_) {
throw new Error("Configuration file .ndmrc not found");
}
Expand All @@ -28,3 +30,12 @@ export async function readConfig() {
throw new Error(`Configuration file invalid (${error.message})`);
}
}

/**
* @typedef {function(string): Promise<string>} ReadFile
*/

/**
* @typedef FileSystem
* @property {ReadFile} readFile
*/
119 changes: 119 additions & 0 deletions src/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (C) 2024 Eric Cornelissen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3 of the License only.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import * as assert from "node:assert/strict";
import { mock, test } from "node:test";

import {
readConfig,
} from "./config.js";

test("config.js", async (t) => {
await t.test("readConfig", async (t) => {
const testCases = {
"empty config": {
config: {},
},
"sample config": {
config: {
"[email protected]": {
"#ignore": true,
},
},
},
"unexpected config": {
config: [
"even though it shouldn't be an array",
"in this part of the code that is not",
"considered a problem",
],
},
};

for (const [name, testCase] of Object.entries(testCases)) {
await t.test(name, async () => {
const fs = createFs({ "./.ndmrc": JSON.stringify(testCase.config) });

const got = await readConfig(fs);
const want = testCase.config;
assert.deepEqual(got, want);
});
}

await t.test("usage of fs.readFile", async () => {
const fs = createFs({ "./.ndmrc": JSON.stringify("{}") });

try {
await readConfig(fs);
} catch (_) { }

assert.equal(fs.readFile.mock.callCount(), 1);

const got = fs.readFile.mock.calls[0];
assert.ok(got.arguments.length >= 1);
assert.match(got.arguments[0], /.ndmrc$/);
});

await t.test("config not in JSON format", async () => {
const fs = createFs({ "./.ndmrc": "I'm not valid JSON" });

await assert.rejects(
async () => {
await readConfig(fs);
},
(error) => {
assert.ok(error instanceof Error);
assert.match(
error.message,
/^Configuration file invalid \(.+?\)$/,
);

return true;
},
);
});

await t.test("file not found", async () => {
const fs = createFs({});

await assert.rejects(
async () => {
await readConfig(fs);
},
(error) => {
assert.ok(error instanceof Error);
assert.equal(
error.message,
"Configuration file .ndmrc not found",
);

return true;
},
);
});
});
});

function createFs(files) {
return {
readFile: mock.fn((path, _options) => {
if (Object.hasOwn(files, path)) {
const content = files[path];
return Buffer.from(content);
} else {
throw new Error();
}
}),
};
}

0 comments on commit 46caab5

Please sign in to comment.