前回のBook モデル RESTAPIの実装にユニットテストを追加する。
Contents
mocha + chai + sinon
mocha
+ chai
を使いモデルとルーティングのユニットテストを実装する。sinon
は今回は使わないがたいていの場合セットで使う場合が多いので一応入れておく。
$ npm install --save-dev mocha
$ npm install --save-dev chai
$ npm install --save-dev chai-http
$ npm install --save-dev sinon
chai-http
はルーティングのアサーションで便利なので入れる
mockdate
カレント時刻をモックするため、MockDateというパッケージを使う
$ npm install --save-dev mockdate
<small>当初sinon.useFakeTimers
を使おうとしたが、うまく使えなかった。</smal>
テストコードのディレクトリ
express プロジェクトのルートディレクトリにtest/
(mocha のデフォルト) を追加してそこに置く。
テストファイル名は*.test.js
とする
src/
bin/
models/
book.js
routes/
book.js
index.js
test/
models/
book.test.js
routes/
books.test.js
helper.js
app.js
package.json
テスト 実行方法
mocha を npm で呼ぶ。package.json
のscripts
に test コマンドを追加する。
package.json
抜粋
{
"scripts": {
"test": "NODE_ENV=test mocha --recursive",
"test:watch": "NODE_ENV=test nodemon --exec mocha --recursive"
}
}
-
全部のテストを実行する場合
$ npm test
-
特定の テストファイルを実行する場合
$ npm test tests/models/book.test.js
-
watch モード。アプリのコード変更を監視してテストを自動実行
npm run
になるので注意$ npm run test:watch
引数のファイルパスは、単一ファイルも指定できるので特定の機能を集中して実装する時など便利。
nodemon
で監視して、mocha コマンドを実行する仕組み。mocha の-w
,--watch
オプションもあるが、mongoose.Schema
で``OverriteError`がでるので nodemon を使う。nodemon の場合は都度新しいプロセスになるので解決する。
mongoose のテスト用 DB 接続
テスト用 DB は環境変数MONGO_URI
に_test
サフィックスを付けたモノにする。
process.env.MONGO_URI + "_test";
helper.js
tests/helper.js
を用意。テストに関する共通処理を実装
const mongoose = require("mongoose");
before(async () => {
// MongoDBに接続
return mongoose
.connect(process.env.MONGO_URI + "_test")
.catch((e) => console.error(e));
});
after(async () => {
// MongoDBをコネクション切断
return mongoose.disconnect().catch((e) => console.error(e));
});
afterEach(async () => {
try {
// すべてのコレクションをdropしてリセットする
const collections = await mongoose.connection.db.collections();
await Promise.all(collections.map(async (collection) => collection.drop()));
} catch (e) {
console.error(e);
}
});
このhelper.js
を各テストファイルで require することでテスト毎に実装せずに済む。
app.js
app.js
の mongoose.connect の部分もテスト環境に合わせられるようにする。
// 環境変数NODE_ENVでテストモードを確認
const isTestEnv = (process.env.NODE_ENV || "").toLowerCase() === "test";
/**
* Connect MongoDB
*/
mongoUri = process.env.MONGO_URI;
// testの場合は_testをsufixにする
if (isTestEnv) mongoUri = mongoUri + "_test";
const mongoose = require("mongoose");
mongoose.connect(mongoUri).catch((e) => {
console.log(e);
});
FactoryGirl
テスト用データ生成ライブラリfactory-girlを使う。Ruby の FactoryBot や Python の FactoryBoy と同じ系。
$ npm install --save-dev factory-girl
tests/facory.js
に定義する。factory.define(name, Schema, attributes)
でデータを定義して、factory.create(name)
でデータ生成するような使い方。
const FactoryGirl = require("factory-girl");
const factory = FactoryGirl.factory;
const Book = require("../models/book");
const adapter = new FactoryGirl.MongooseAdapter();
// use the mongoose adapter as the default adapter
factory.setAdapter(adapter);
factory.define("Book", Book, {
title: factory.sequence("Book.title", (n) => `タイトル${n}`),
author: factory.sequence("Book.author", (n) => `著者${n}`),
year: 2022,
pages: 100,
});
module.exports = factory;
後述のルーティングのテストで使っている。
モデルのユニットテスト
Book
モデルをテストする。tests/models/test_book.js
const helper = require("../helper"); // 共通処理
const Book = require("../../models/book");
const assert = require("chai").assert;
const mockdate = require("mockdate");
describe("Book Model", () => {
beforeEach((done) => {
// カレント時刻をmockする
mockdate.set(new Date(2022, 9, 11, 14, 55));
done();
});
afterEach((done) => {
// mockをクリアする。しないとは他のテストケースに影響する
mockdate.reset();
done();
});
it("Bookが登録できること", async () => {
let book = new Book({
title: "foo",
author: "bar",
year: 2022,
pages: 20,
});
let err = book.validateSync();
assert.isUndefined(err);
await book.save();
book = await Book.findById(book.id);
assert.strictEqual(book.title, "foo");
assert.strictEqual(book.author, "bar");
assert.strictEqual(book.year, 2022);
assert.strictEqual(book.pages, 20);
assert.strictEqual(
book.createdAt.getTime(),
new Date(2022, 9, 11, 14, 55).getTime()
);
assert.strictEqual(
book.updatedAt.getTime(),
new Date(2022, 9, 11, 14, 55).getTime()
);
});
it("ValidateErrorがある場合エラーになること", async () => {
let book = new Book({
title: "",
author: "bar",
year: 2022,
pages: 20,
});
try {
await book.save();
} catch (err) {
assert.isTrue("title" in err.errors);
}
});
describe("Validation", () => {
it("titleはがない場合はエラーになること", () => {
let book = new Book({
title: "",
author: "bar",
year: 2022,
pages: 20,
});
let err = book.validateSync();
assert.isTrue("title" in err.errors);
});
it("authorがない場合はエラーになること", () => {
let book = new Book({
title: "foo",
author: "",
year: 2022,
pages: 20,
});
let err = book.validateSync();
assert.isTrue("author" in err.errors);
});
it("yearがない場合はエラーになること", () => {
let book = new Book({
title: "foo",
author: "bar",
year: null,
pages: 20,
});
let err = book.validateSync();
assert.isTrue("year" in err.errors);
});
it("pagesがない場合はエラーになること", () => {
let book = new Book({
title: "foo",
author: "bar",
year: 2022,
pages: null,
});
let err = book.validateSync();
assert.isTrue("pages" in err.errors);
});
});
});
ルーティングのテスト
Book
の REST API のテストをtests/routes/test_book.js
に実装する
const helper = require("../helper");
const factory = require("../factory");
const Book = require("../../models/book");
const app = require("../../app");
const assert = require("chai").assert;
const chai = require("chai");
chai.use(require("chai-http"));
describe("Book Router", () => {
describe("GET /books/", () => {
beforeEach(async () => {
// テストデータ 3件生成する。factory-girl便利。
await factory.createMany("Book", 3);
});
it("Bookの一覧を返すこと", (done) => {
chai
.request(app)
.get("/books/")
.end((err, res) => {
assert.equal(res.status, 200);
assert.equal(res.body.length, 3);
done();
});
});
});
});
c8 でカバレッジを取得
コードカバレッジを取得するのにはc8を使う。
$ npm insall --save-dev c8
package.json
の script で test のコマンドをでc8
コマンド経由でmocha
を実行
するように修正する
{
"scripts": {
"test": "NODE_ENV=test c8 mocha --recursive --exit",
"test:watch": "NODE_ENV=test nodemon --exec c8 mocha --recursive --exit",
}
}
c8
コマンドのオプションはいろいろ指定できるけど、コマンドが長くなるので.c8rc.json
という設定ファイルで指定する。.c8rc.json
はデフォルトで読み込んでくれるファイル名。
.c8rc.json
{
"all": false,
"reporter": [
"text-summary",
"html"
],
"include": [
],
"exclude": [
`test/**/*.js
]
}
オプションはドキュメントを参照のこと
all
: テストスイートで触れないファイルのカバレッジを取得するか否かreporter
: 結果の出力方式。text-summary
は標準出力にサマリを表示。html
は/coverage
ディレクトリに html で出力。他にも指定できる。include
: カバレッジ対象のホワイトリスト。ファイルパスを指定する。**
,*
ワイルドカード可exclude
: カバレッジ対象の除外リスト。前述のinclude
かexclude
でカバレッジ対象を絞る。
node_modules
ディレクトリはデフォルトで除外してくれるのでexclude
に指定する必要はない。