Atlas Function のテスト
項目一覧
このページでは、Atlas Function をテストするために使用できるいくつかの方法について説明します。
関数 JavaScript ランタイムと標準の Node.js ランタイムは異なるため、関数をテストする際にはいくつか独自の考慮事項を考慮する必要があります。 このページでは、関数の一意性を処理する方法について説明します。
始める前に
Atlas Function をテストするには、次のものが必要です。
Atlas App Services アプリ。 作成方法については、「アプリの作成 」を参照してください。
アプリを構成するための コード配置 メソッド。 次のいずれかを選択します。
ローカル システム
PATH
にインストールおよび追加された App Services CLI のコピー。 詳しくは、「 App Services CLI のインストール 」を参照してください。アプリの 構成ファイル を保持して配置するように構成されたGithubリポジトリ 。 その設定方法については、「 Githubを使用した自動配置 」を参照してください。
関数のユニットテスト
ユニット テストを使用して、関数の機能を検証できます。 Node.js 互換の 関数をテストするためのテストフレームワーク。 このページの例では、 Jest テスト フレームワークを使用します。
CommonJS モジュール を使用する必要があります : 関数のユニット テストを作成します。
関数コードの書込み
関数コードを簡単にテストできるようにするには、個別のコンポーネントに分割してモジュール型にします。 関数のすべてのロジックは、前の手順で定義したファイルに保持する必要があります。 関数 ファイルでは、プロジェクト内の他のファイルからの相対インポートは実行できません。 npm を使用して依存関係をインポートすることもできます。
関数をエクスポートするには、 exports
に割り当てて関数をエクスポートする必要があります。
function greet(word) { return "hello " + word; } function greetWithPunctuation(word, punctuation) { return greet(word) + punctuation; } // Function exported to App Services exports = greetWithPunctuation;
ユニット テストで使用するためのエクスポート関数
Node.js ユニットのテスト ファイルで使用するコードをエクスポートするには、CommonJS module.exports
構文を使用する必要があります。
この構文は 関数のランタイムと互換性がありません。 Atlas Functions 環境では、Node.js グローバルmodule
は提供されません。 関数と互換性のあるファイルを維持しながらユニット テストにモジュールをエクスポートするには、 module.exports
ステートメントを チェックでラップし、グローバルmodule
オブジェクトが存在するかどうかを確認します。
function greet(word) { return "hello " + word; } function greetWithPunctuation(word, punctuation) { return greet(word) + punctuation; } // Function exported to App Services exports = greetWithPunctuation; // export locally for use in unit test if (typeof module !== "undefined") { module.exports = { greet, greetWithPunctuation }; }
エクスポートされた 関数 コードのユニット テスト
これで、 関数 ファイルからエクスポートしたモジュールのユニット テストを記述できます。 プロジェクト内の別のtest
ディレクトリに関数ファイルのテスト ファイルを作成します。
mkdir -p test/unit touch test/unit/hello.test.js
前のステップでエクスポートしたモジュールをインポートし、ユニット テストを追加します。
const { greet, greetWithPunctuation } = require("../../functions/hello"); test("should greet", () => { const helloWorld = greet("world"); expect(helloWorld).toBe("hello world"); }); test("should greet with punctuation", () => { const excitedHelloWorld = greetWithPunctuation("world", "!!!"); expect(excitedHelloWorld).toBe("hello world!!!"); });
モック サービス
グローバル コンテキスト オブジェクトまたは関数が公開するその他のグローバル モジュールのいずれかを使用する関数のユニット テストを作成するには、その動作のモックを作成する必要があります。
この例では、関数はcontext.values.get()
経由でApp Services Valueを参照し、グローバル モジュールBSON を使用して ObjectId を作成します。
function accessAppServicesGlobals() { const mongodb = context.services.get("mongodb-atlas"); const objectId = BSON.ObjectId() // ... do stuff with these values } exports = accessAppServicesGlobals; if (typeof module !== "undefined") { module.exports = accessAppServicesGlobals; }
これらのモックを Node.js グローバル名前空間にアタッチします。 これにより、関数ランタイムでの方法と同じ方法でユニット テストでモックを呼び出すことができます。
global.context = { // whichever global context methods you want to mock. // 'services', 'functions', values, etc. } // you can also mock other Functions global modules global.BSON = { // mock methods }
また、グローバル名前空間を汚染しないように、セットアップ ブロックと引き継ぎブロックでこれらのモックを宣言して削除することもできます。
// adds context mock to global namespace before each test beforeEach(() => { global.context = { // your mocking services }; }); // removes context from global namespace after each test afterEach(() => { delete global.context; }); test("should perform operation using App Services globals", () => { // test function that uses context });
例
コンテキストにアクセスする関数のモック作成
この例の関数は、App Services 値にアクセスし、それを返します。
function greet() { const greeting = context.values.get("greeting"); // the greeting is 'beautiful world' return "hello " + greeting; } exports = greet; if (typeof module !== "undefined") { module.exports = greet; }
テスト ファイルhelloWithValue.test.js
を作成します。 テストファイルには、次のものが含まれています。
helloWithValue.js
からエクスポートされた関数をインポートします。context.values.get()
のモック。 グローバル名前空間を破損しないようにブロックをセットアップします。モックを使用するインポートされた関数のテスト。
// import the function const greet = require("../../functions/helloWithValue"); // wrap the mock in beforeEach/afterEach blocks to avoid // pollution of the global namespace beforeEach(() => { // mock of context.values.get() global.context = { values: { get: (val) => { const valsMap = { greeting: "magnificent morning", }; return valsMap[val]; }, }, }; }); afterEach(() => { // delete the mock to not pollute global namespace delete global.context; }); // test function using mock test("should greet with value", () => { const greeting = greet(); expect(greeting).toBe("hello magnificent morning"); });
関数の統合テスト
すべての関数は、本番環境にデプロイする前に、統合テストを実行する必要があります。 Atlas Function JavaScript ランタイムは標準の Node.js ランタイムと異なるため、これは特に重要です。 App Services に配置された関数をテストしないと、予期しないエラーが発生する可能性があります。
関数の統合テストを記述するには単一の方法はありません。 関数は異なる目的でさまざまなコンテキストで使用できるため、それぞれのユースケースには異なる統合テスト戦略が必要です。
たとえば、Device SDK クライアントから呼び出す関数の統合テストを作成する方法は、データベースtrigger関数をテストする方法とは異なります。
ただし、 関数 の統合テストを作成するには一般的な手順がいくつかあります。 大まかレベルでは、これらの手順は次のようになります。
本番アプリと同じ構成でテスト アプリを作成します。
ライブ テスト環境に配置された関数を操作する統合テストを作成します。
このセクションの残りの部分では、アプリに 統合テスト を実装する方法について、より詳細に説明します。
Tip
以下も参照してください。
関数 JavaScript ランタイムの固有の要素の詳細については、以下を参照してください。
関数のさまざまなユースケースの詳細については、 「 関数を使用する場合 」を参照してください。
テスト アプリを作成する
データソースとバックエンド構成が異なることを除いて、本番アプリと同じ構成を持つテスト目的のアプリを作成します。
同じ構成を持つ複数のアプリを作成する方法の詳細については、「アプリ環境の構成 」を参照してください。
例
データベースtrigger関数のテスト
この例では、Realm Node.js SDKと Jest テスト フレームワークを使用して、データベース trigger をテストします。
trigger 関数は マテリアライズドビュー を作成します 新しい販売が行われるたびに、製品の合計売上のが増加します。
エントリがsales
テーブルに追加されるたびに trigger が起動します。 total_sales_materialized
テーブルのtotal_sales
フィールドが 1 インクリメントされます。
データベースtriggerの構成は次のとおりです。
{ "id": "62bb0d9f852c6e062432c454", "name": "materializeTotalSales", "type": "DATABASE", "config": { "operation_types": ["INSERT"], "database": "store", "collection": "sales", "service_name": "mongodb-atlas", "match": {}, "project": {}, "full_document": true, "full_document_before_change": false, "unordered": false, "skip_catchup_events": false }, "disabled": false, "event_processors": { "FUNCTION": { "config": { "function_name": "materializeTotalSales" } } } }
trigger は次の関数を呼び出します。
exports = function (changeEvent) { const { fullDocument: { productId }, } = changeEvent; const totalSalesMaterialization = context.services .get("mongodb-atlas") .db("store") .collection("total_sales_materialized"); totalSalesMaterialization.updateOne( { _id: productId }, { $inc: { total_sales: 1 } }, { upsert: true } ); };
この例では、 Node.js Realm SDKを使用して trigger をテストし、MongoDB Atlas と対話します。 また、Realm MongoDBQueryAPI またはMongoDB ドライバー のいずれかを持つ任意の SDKMongoDB Atlas を使用して、 をクエリし、 データベースtrigger をテストすることもできます。
const { app_id } = require("../../root_config.json"); const Realm = require("realm"); const { BSON } = require("realm"); let user; const app = new Realm.App(app_id); const sandwichId = BSON.ObjectId(); const saladId = BSON.ObjectId(); // utility function async function sleep(ms) { await new Promise((resolve) => setTimeout(resolve, ms)); } // Set up. Creates and logs in a user, which you need to query MongoDB Atlas // with the Realm Node.js SDK beforeEach(async () => { const credentials = Realm.Credentials.anonymous(); user = await app.logIn(credentials); }); // Clean up. Removes user and data created in the test. afterEach(async () => { const db = user.mongoClient("mongodb-atlas").db("store"); await db.collection("sales").deleteMany({}); await db.collection("total_sales_materialized").deleteMany({}); await app.deleteUser(user); }); test("Trigger creates a new materialization", async () => { const sales = user .mongoClient("mongodb-atlas") .db("store") .collection("sales"); await sales.insertOne({ _id: BSON.ObjectId(), productId: sandwichId, price: 12.0, timestamp: Date.now(), }); // give time for the Trigger to execute on Atlas await sleep(1000); const totalSalesMaterialized = user .mongoClient("mongodb-atlas") .db("store") .collection("total_sales_materialized"); const allSandwichSales = await totalSalesMaterialized.findOne({ _id: sandwichId, }); // checks that Trigger increments creates and increments total_sales expect(allSandwichSales.total_sales).toBe(1); }); test("Trigger updates an existing materialization", async () => { const sales = user .mongoClient("mongodb-atlas") .db("store") .collection("sales"); await sales.insertOne({ _id: BSON.ObjectId(), productId: saladId, price: 15.0, timestamp: Date.now(), }); await sales.insertOne({ _id: BSON.ObjectId(), productId: saladId, price: 15.0, timestamp: Date.now(), }); // give time for Trigger to execute on Atlas await sleep(1000); const totalSalesMaterialized = user .mongoClient("mongodb-atlas") .db("store") .collection("total_sales_materialized"); const allSaladSales = await totalSalesMaterialized.findOne({ _id: saladId, }); // checks that Trigger increments total_sales for each sale expect(allSaladSales.total_sales).toBe(2); });