测试 Atlas Function
本页介绍了一些可用于测试 Atlas Function 的策略。
由于 Functions JavaScript 运行时和标准 Node.js 运行时之间存在差异,因此在测试函数时必须考虑一些独特的注意事项。本页介绍如何处理函数的唯一性。
开始之前
您需要使用以下内容以测试 Atlas Function:
Atlas App Services 应用程序。要了解如何创建应用程序,请参阅创建应用程序。
用于配置应用程序的代码部署方法。选择以下方法之一:
已安装 App Services CLI 副本,并将其添加到本地系统
PATH
中。要了解操作方法,请参阅安装 App Services CLI。将 GitHub 存储库配置为保存并部署应用程序的配置文件。要了解如何设置存储库,请参阅使用 GitHub 自动部署。
函数的单元测试
您可以使用单元测试来验证函数的功能。 使用任何与 Node.js 兼容的 用于测试函数的测试框架。 此页面上的示例使用 Jest 测试框架。
您必须使用 CommonJS 模块为函数编写单元测试。
创建新函数
创建一个新函数。在应用程序的配置文件中,在函数的 functions
目录中创建新的 JavaScript 文件。
touch functions/hello.js
您还需要将函数的配置信息添加到 functions/config.json
中。
{ "name": "hello", "private": false, "run_as_system": true },
提示
有关创建新函数的更多信息,请参阅定义函数。
编写函数代码
为了使函数代码易于测试,请将其关注点分散到不同的组件以保持模块化。您必须将函数的所有逻辑保留在上一步定义的文件中。您无法在函数文件中从项目中的其他文件执行相对导入。您也可以使用 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 Function 环境不提供 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 值,并使用全局模块 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函数
此示例使用 Realm Node.js SDK 和 Jest 测试框架来测试数据库触发器。
每次进行新销售时,触发器函数都会创建产品总销售额的物化视图。
每次在 sales
表中添加条目时,都会触发触发器。它将 total_sales_materialized
表上的 total_sales
字段增加 1。
数据库触发器具有以下配置:
{ "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 } ); };
此示例使用trigger Node.jsRealm SDK MongoDB Atlas与 交互来测试 。您还可以使用任何具有Realm MongoDBQuery 的 SDKAPI 或其中一个MongoDB 驱动程序MongoDB 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); });