Test Atlas Functions
On this page
This page describes some strategies you can use to test your Atlas Functions.
Due to differences between the Functions JavaScript runtime and the standard Node.js runtime, you must take some unique considerations into account when testing Functions. This page covers how to handle the uniqueness of Functions.
Before You Begin
You will need the following to test an Atlas Function:
An Atlas App Services App. To learn how to create one, see Create an App.
A code deploy method to configure your App. Choose one of:
A copy of App Services CLI installed and added to your local system
PATH
. To learn how, see Install App Services CLI.A GitHub repository configured to hold and deploy configuration files for your App. To learn how to set one up, see Deploy Automatically with GitHub.
Unit Tests for Functions
You can validate the functionality of your Functions with unit tests. Use any Node.js-compatible testing framework to test Functions. The examples on this page use the Jest testing framework.
You must use CommonJS modules to write unit tests for Functions.
Create a new Function
Create a new Function. In the App's configuration files, create a new JavaScript
file in the functions
directory for your function.
touch functions/hello.js
You also need to add configuration information for the Function to functions/config.json
.
{ "name": "hello", "private": false, "run_as_system": true },
Write Function code
To make your Function code easy to test, keep it modular by separating its concerns into distinct components. You must keep all logic for the Function in the file you defined in the previous step. You cannot perform relative imports from other files in your project in a Function file. You can also import dependencies using npm.
You must export your function by assigning it to exports
.
function greet(word) { return "hello " + word; } function greetWithPunctuation(word, punctuation) { return greet(word) + punctuation; } // Function exported to App Services exports = greetWithPunctuation;
Export Function for use in unit tests
To export your code to use in separate Node.js unit test files,
you must use CommonJS module.exports
syntax.
This syntax is not compatible with the Functions runtime.
The Atlas Functions environment does not provide the Node.js global module
.
To export modules to your unit tests while keeping the file compatible with
Functions, wrap the the module.exports
statement
with a check to see if the global module
object exists.
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 }; }
Unit test exported Function code
Now you can write unit tests for the modules that you exported from the
Function file. Create a test file for the Function file in
a separate test
directory somewhere in your project.
mkdir -p test/unit touch test/unit/hello.test.js
Import the modules you exported in the previous step and add unit tests.
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!!!"); });
Mock Services
To write unit tests for Functions that use the global context object or one of the other global modules that Functions expose, you must create mocks of their behavior.
In this example, the Function references an App Services Value
via context.values.get()
and creates an ObjectId using the global module
BSON.
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; }
Attach these mocks to the Node.js global namespace. This lets you call the mocks in your unit tests the same way you do in the Functions runtime.
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 }
You may also want to declare and remove these mocks in setup and teardown blocks so that they do not pollute the global namespace.
// 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 });
Example
Mocking a Function that accesses context
The function in this example accesses an App Services Value and returns it.
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; }
Now create a test file helloWithValue.test.js
.
The test file contains the following:
Import the function exported from
helloWithValue.js
.A mock of
context.values.get()
. Wrap the mock in set up and tear down blocks so that it does not pollute the global namespace.A test of the imported function that uses the mock.
// 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"); });
Integration Tests for Functions
You should perform integration tests on all Functions before deploying them to production environments. This is especially important because the Atlas Function JavaScript runtime differs from the standard Node.js runtime. Unexpected errors can occur if you do not test functions deployed to App Services.
There is no single way to write integration tests for Functions. As Functions can be used in a variety of different contexts for different purposes, each use case requires a different integration testing strategy.
For example, the way you create an integration test for a Function that you invoke from a Device SDK client is different from the way you would test a Database Trigger Function.
However, there are some general steps that you can take to writing integration tests for Functions. On a high level these steps are:
Create a testing App with the same configuration as your production App.
Write integration tests that interact with your Functions deployed to a live testing environment.
The remainder of this section explains how to implement integration tests for your App in more detail.
Tip
See also:
For more information on the unique aspects of the Functions JavaScript runtime, refer to:
For more information on the different use cases for Functions, refer to When to Use Functions.
Create a test App
Create an App for testing purposes that has the same configuration as your production App, except using different data sources and backend configuration.
For more information on how you can create multiple Apps with the same configuration, see Configure an App Environment.
Test in live environment
Once you have deployed your test App, test its functionality using your preferred testing language and framework.
The :ref:Realm client SDKs are useful for testing Apps. These SDKs provide first-class access to App Services. In your testing suite, you can connect to your testing App with a Realm SDK. Test the interaction with the App using the Realm SDK.
Example
Testing a Database Trigger Function
This example uses the Realm Node.js SDK and the Jest testing framework to test a Database Trigger.
The Trigger Function creates a materialized view of total sales for a product whenever a new sale is made.
The Trigger fires every time an entry is added to the sales
table.
It increments the total_sales
field on the total_sales_materialized
table
by one.
The Database Trigger has the following configuration:
{ "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" } } } }
The Trigger invokes the following Function:
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 } ); };
This example tests the Trigger using the Node.js Realm SDK to interact with MongoDB Atlas. You can also use any Realm SDK with the MongoDB Query API or one of the MongoDB drivers to query MongoDB Atlas to test a Database 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); });