Docs Menu

Docs HomeAtlas App Services

Test Atlas Functions

On this page

  • Before You Begin
  • Unit Tests for Functions
  • Get a local copy of your App Services App
  • Create a new Function
  • Write Function code
  • Export Function for use in unit tests
  • Unit test exported Function code
  • Mock Services
  • Integration Tests for Functions
  • Create a test App
  • Test in live environment

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.

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.

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.

1
2

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
},

Tip

See also:

For more information on creating a new Function, refer to Define a Function.

3

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.

hello.js
function greet(word) {
return "hello " + word;
}
function greetWithPunctuation(word, punctuation) {
return greet(word) + punctuation;
}
// Function exported to App Services
exports = greetWithPunctuation;
4

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.

functions/hello.js
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 };
}
5

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.

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!!!");
});

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.

accessAppServicesGlobals.js
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.

helloWithValue.js
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.

helloWithValue.test.js
// 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");
});

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:

  1. Create a testing App with the same configuration as your production App.

  2. 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.

1

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.

2

Once you have deployed your test App, test its functionality using your preferred testing language and framework.

The 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:

triggers/materializeTotalSales.json
{
"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:

functions/materializeTotalSales.js
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.

test/integration/materializeTotalSales.test.js
const { app_id } = require("../../realm_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);
});
←  External DependenciesHandle Errors in Functions →