5 分でわかる Javascript の単体テスト
JavaScript アプリの単体テストとコード カバレッジのセットアップ
アプリの構築には、要件の収集から最終的にアプリケーションを運用環境にデプロイするまで、さまざまな手順が必要です。これらの手順を迅速に進めたいとはいえ、時間を節約するためにいくつかの手順を省略することはあまりお勧めできません。開発段階には好き嫌いが分かれるこのようなステップが数多くあり、通常 「単体テストはあまり好まれない」のです。
ここで、単体テストとは何なのか、なぜそれについて話しているのかといった疑問がほとんど出てくるかもしれません。単体テストを書くことの重要性は何ですか? これらは有効な質問であり、この記事でも同じことに答えてみました。この記事を最後まで読むと、きっと理解が深まると思います。
単体テストとは何ですか? 単体テストを書くことの重要性は何ですか?
名前が示すように、単体テストは、ソフトウェア アプリケーションの特定のユニットまたはコンポーネントをテストするために行われます。単体テストの主な目的は、コードが期待どおりに動作するかどうかをテストし、不要なバグを防ぐことです。単体テストを適切に作成すると、コードが保守しやすく、新しい機能を追加する際に既存の機能が損なわれないことも保証されます。
例: 既存の関数の条件を更新するタスクを受け取ったシナリオを考えてみましょう。単純な変更なので、安心して実行できました。ロジックの変更により本番環境で何らかの問題が発生するまではすべてが正常に動作していましたが、この種の問題はアプリにとって致命的なものになる可能性があります。単体テストを完璧に記述していれば、コード変更の影響を事前に判断することで、この状況の発生を簡単に防ぐことができたはずです。
単体テストというキーワードを聞くと、これはテスト段階で行われると思われるかもしれませんが、そうではありません。単体テストは、コードの他の部分と同じように、コード (アプリケーション) の一部である必要があります。
この記事ではさらに、node.js アプリの単体テストを簡単に設定し、アプリのコード カバレッジを生成する方法を見ていきます。
サンプルNode.js アプリ
この例では、単純な計算機であり、加算、減算、乗算、除算などの操作を実行する API で構成されるサンプルの node.js アプリを使用します。上記の操作に加えて、DB 操作を示す例も含めました。これについては、この記事で詳しく説明します。
- calculator.js:このファイルには、加算、減算、乗算、除算などのさまざまな電卓操作のためのさまざまな関数が含まれています。
- Index.js : これには、電卓のさまざまな操作を実行するための API が含まれています。
- DB.js : DB接続やDBから値を取得するためのダミー関数が含まれます。
これで、サンプルの node.js アプリの準備が整いました。アプリによって公開されているさまざまな API を実行して呼び出すことで、postman で「SampleNodeApp」をテストできます。
しかし、API を手動でテストするだけで十分でしょうか? おそらく: この小さなアプリではその通りですが、アプリのサイズが大きくなり続けると、さまざまな入力セットでアプリをテストし、新しい変更によって既存のコードが破壊されないことを確認することがますます困難になります。そこで単体テストが役に立ちます。
使用したライブラリ
単体テストでは、このアプリでは次のライブラリを使用します。
- Mocha : これは、Node.js への単純な JavaScript テスト ライブラリです。Mocha テストはシリアルに実行されるため、捕捉されなかった例外を正しいテスト ケースにマッピングしながら、柔軟で正確なレポートを作成できます。Mocha の詳細については、「 」をご覧ください。
- Chai : これは、さまざまな JavaScript フレームワークと組み合わせることができる単体テスト アサーション フレームワークです。Chai は、さまざまな種類のメソッドをテストするためのさまざまなプラグインも提供します。API のテストには、 chai-httpプラグインを使用します。チャイの詳細については、「 」をご覧ください。
- Sinon:これは、人気のある JavaScript 単体テストのモック フレームワークです。Sinon は、テスト中にいくつかの事前定義された仮定を立てて、さまざまな関数の動作を模擬するのに役立ちます。たとえば、単体テスト中に DB を呼び出したくない場合があります。この場合、Sinon は DB 関数のモックを作成するのに役立ちます。シノンの詳細については、「 」をご覧ください。
- ニューヨーク市/イスタンブール: 単体テストを作成した後、各ブランチとケースをカバーするテストを作成したかどうかを確認したい場合があります。この目的のために、コード カバレッジ レポートを生成できます。NYC は、単体テストのコード カバレッジの生成を支援します。一方、イスタンブールは、HTML およびその他のサポートされている形式で詳細レポートを生成するのに役立ちます。
さまざまな単体テスト ライブラリについて理解したので、アプリケーションの単体テストの作成を開始できます。
サンプルアプリテスト
mocha フレームワークを使用して作成されたすべてのテストは、デフォルトでメイン ディレクトリの下のtestディレクトリに作成されます。まず、新しいファイルtest/SampleAppTests.js を作成します。
- Imports : これは最も単純な部分であり、他のnode.jsファイルと同様に、このファイルに必要な依存関係とモジュールをインポートする必要があります。
- Chai プラグイン: この記事のライブラリ セクションで説明したように、chai フレームワークではさまざまな既存のプラグインが許可され、独自のプラグインを作成する機能もあります。現在のユースケースでは、chai-http と should の 2 つのプラグインを使用します。
- description/it : description は、テストを人間が読めるセクションに作成または分割するために使用されます。必要に応じて、 describe を使用して階層レベルをいくつでも定義できます。上の例では、テストをさまざまなカテゴリに分割しています。これは、mocha でテストを定義して実行するために使用されます。テスト用のコマンドを実行すると、実際の関数がテストを実行します。
- ネガティブなテスト: テストを作成し、アプリケーション内の機能のポジティブなケースをすべてカバーすることに忙しいうちに、テストのネガティブなケースをカバーすることを忘れがちです。ネガティブなケースをカバーしないと、カバー範囲が狭くなり、重要な問題が見逃される可能性があります。したがって、ネガティブな単体テストは、ポジティブなテストを作成するのと同じくらい重要です。
- モッキング:モックを理解することは非常に重要であり、DB やその他のデータ ソースを処理する関数をテストする場合には必要になる場合もあります。上記の例では、DB と対話する関数をモックしています。モック関数の場合、この関数が常にモックされた結果を返すといういくつかの前提を設定する必要があります。また、モックされた関数はカバーされているとはみなされず、全体のカバレッジが低下するため、どの関数をモックする必要があるかを賢明に選択してください。経験則として、アプリケーションの内部にあり、DB や外部ソースと対話しない関数のモックは避ける必要があります。
テストの実行
テストを実行するために、提供された package.json ファイルを使用している場合、スクリプトはすでに追加されています。スクリプトを設定したら、次のコマンドを実行するだけです。
npm test
出力を見ましたか? おめでとうございます!!!! ほぼ完成しましたが、まだ完全ではありません。
ボーナスチップ
DB.js のコード カバレッジが赤で表示されていることに気付きました。これにより、全体的なコード カバレッジも低下します。これは、テスト ケースを作成するために作成したモック関数によるものです。では、解決策は何でしょうか?
多くの場合、コード カバレッジで考慮されないように、一部のファイルやコードの一部を除外または含めたい場合があります。NYC は、数行の設定で同じことを簡単に実行できる機能を提供します。このためには、プロジェクトの親ディレクトリに.nycrcを追加するだけです。include/exclude に加えて、.nycrc は他のいくつかの機能も提供します。
上記の構成ファイルを追加した後の出力を見てみましょう。ご覧のとおり、不要なファイルを削除した後、カバレッジは 100% に増加しました。
上記のコードを複製できたら、テストを実行してコード カバレッジを生成します。素晴らしい仕事です。
同じことを実行して再現できなかった人も、心配する必要はありません。次のセクションでは、すべての重要なファイルのコードを追加しました。
//calculator.js
function calculator(operation, num1, num2) {
let res;
switch (operation) {
case "add":
res = add(num1, num2);
break;
case "subract":
res = subract(num1, num2);
break;
case "multiply":
res = multiply(num1, num2);
break;
case "divide":
res = divide(num1, num2);
break;
}
console.log(res);
return new String(res);
}
function add(num1, num2) {
return num1 + num2;
}
function subract(num1, num2) {
return num1 - num2;
}
function multiply(num1, num2) {
return num1 * num2;
}
function divide(num1, num2) {
return num1 / num2;
}
module.exports = { calculator };
//index.js
const express = require("express");
const { calculator } = require("./calculator");
const DB = require("./DB");
const app = express();
const port = 3000;
const db=new DB()
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
app.get("/calculator/:operation", validateOperation(),(req, res) => {
let num1 = req.query.num1;
let num2 = req.query.num2;
let operation = req.params.operation;
if (!operation || !num1 || !num2) return "invalid values";
return res.send(calculator(operation, Number(num1), Number(num2)));
});
app.get("/DBcalculator", (req, res) => {
let val=db.calculateFromDb()
return val;
})
//middleware for validation
function validateOperation() {
const errorMessage =
"Invalid operations: operations allowed add, subract, multiply, divide";
const validOperation = ["add", "subract", "multiply", "divide"];
return (req, res, next) => {
let operation = req.params.operation;
if (!operation || !validOperation.includes(operation)) {
return res.status(400).json({ Message: errorMessage });
}
next();
};
}
module.exports = app
//DB.js
const sql = require("mysql");
const credentials = {
host: "localhost",
user: "me",
password: "secret",
database: "my_db",
};
module.exports = class DB {
//Get the DB Connection
static getConnection() {
return sql.createConnection(credentials);
}
//DB operation
calculateFromDb() {
const connection = DB.getConnection();
connection.query(
"SELECT 1 + 1 AS solution",
function (error, results, fields) {
if (error) throw error;
console.log("The solution is: ", results[0].solution);
}
);
return "Value from DB";
}
};
//package.json
{
"name": "samplenodeapp",
"version": "1.0.0",
"description": "A sample for testing unit tests",
"main": "index.js",
"scripts": {
"test": "nyc --reporter=clover --reporter=text mocha --exit --timeout 50000"
},
"author": "Jaideep Pahwa",
"license": "ISC",
"devDependencies": {
"chai": "^4.3.7",
"chai-http": "^4.3.0",
"express": "^4.18.2",
"istanbul": "^0.4.5",
"mocha": "^10.1.0",
"nyc": "^15.1.0",
"sinon": "^14.0.1"
},
"dependencies": {
"mysql": "^2.18.1"
}
}
//test/SampleAppTests.js
const { should } = require("chai");
let chai = require("chai");
// required for testing the api
let chaiHttp = require("chai-http");
//required for mocking methods
const sinon = require("sinon");
const DB = require("../DB.js");
chai.use(chaiHttp);
chai.use(should);
const server = require("../index.js");
describe("Sample app test cases", () => {
describe("Testing calculator API", () => {
let num1 = 2,
num2 = 1;
//positive test cases
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/add?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("3");
});
done();
});
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/subract?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("1");
});
done();
});
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/multiply?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("2");
});
done();
});
it("test caclulator for valid id inputs", (done) => {
chai
.request(server)
.get(`/calculator/divide?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
res.body.should.be.eql("2");
});
done();
});
});
describe("Testing calculator negative cases API", () => {
let num1 = 2,
num2 = 1;
//negative test cases
it("test caclulator for invalid id inputs: invalid operation", (done) => {
chai
.request(server)
.get(`/calculator/addd?num1=${num1}&num2=${num2}`)
.end((err, res) => {
res.should.have.status(400);
res.body.should.be.a("object");
});
done();
});
});
describe("Testing calculator API", () => {
//Testing DB methods by mocking
it("test the db methods by mockning functions", (done) => {
sinon.stub(DB.prototype, "calculateFromDb").returns("Value from DB");
chai
.request(server)
.get(`/DBcalculator`)
.end((err, res) => {
res.should.have.status(200);
res.body.should.be.a("string");
});
done();
});
});
});
//.nycrc
{
"include": [
"*.js"
],
"exclude": [
"DB.js"
],
"check-coverage": true,
"lines": 65
}
この記事では、単体テストの概要から始めて、その重要性について理解しました。次に、サンプルのnode.jsアプリを調べ、さまざまなJavaScriptフレームワークを使用してnode.jsアプリの単体テストを作成する方法を調べました。おまけのヒントとして、不要なコードとファイルを削除してコード カバレッジを増やす方法も検討しました。
私の作品を気に入っていただけましたら、この記事を「いいね!」して共有してください(無料です :) )。また、このような記事をもっと見るために私をフォローしてください。
また、私の他の記事もチェックしてください。

![とにかく、リンクリストとは何ですか?[パート1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































