Guest Tutorials

How to integrate tests into your Parse Cloud Code functions

Introduction

This is a guide written by John Considine, our guest writer. The tutorial covers how to setup automated tests for your Back4App Cloud Code.

We will talk briefly about moving some of your client Parse code to the cloud, and then about how integrate your project with a testing ecosystem. You can also check out the example project directly for a working version.

Goals

We hope to combine the robust, scalable aspects of automated tests with the developer-friendly Parse environment. By leveraging Cloud Code, perhaps an underappreciated feature of Parse, developers can continue to rapidly iterate their code AND be confident that the software will run as expected.

Test Driven Development is an immense field; rather than talk philosophically about testing, we will run through an implementation and talk about some strategies (stubbing for instance).

Prerequisites

To complete this tutorial, you need:

  • An app created at Back4App.
  • Back4App Command Line Configured with the project
  • npm installed on your command line

Note: This library will use the JavaScript Promise, which shouldn’t be too complicated

Let’s create a basic social media backend

Posting Ap

Ok! Imagine a social media application that includes a profile model to go along with the user model. For some apps, you may place profile information in the user model, although in many cases this is not efficient; you often will need to separate the concerns of authorization / authentication with user content, and thus maintain two different models.

In this tutorial, we will be implementing a feature that manages the creation of users and profiles in this way, placing minimal strain on the client. Let’s get started!

1) Defining Our Functions

This assumes you have a Back4App project created, and the command line tool installed (see prerequisites).

For examples of frontend code, this guide will be referring to the Parse JavaScript SDK syntax for simplicity

When someone signs up for this application, a profile should be created and coupled to the user object.

The signup function

On many Parse applications, you will create the user with the following syntax

1
2
3
4
var user = new Parse.User();
user.set("username", "my name");
user.set("password", "my pass");
user.set("email", "[email protected]");

In our case, we would like to also initialize a Profile, and point it at the User object.

1
2
3
4
5
6
7
8
9
10
11
12
13
user.signUp(null, {
  success : function (newUser) {
    var Profile = Parse.Object.extend("Profile");
    var profile = new Profile();
    profile.set("firstname", "John");
    profile.set("lastname", "Smith");
    profile.set("user", newUser);
    profile.save();
  },
  error : function (err) {
    // handle error
  }
})

You could shorten that syntax to be something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var user = new Parse.User({
  username : params.username,
  password : params.password,
  email : params.email
});
user.signUp(null)
.then((newUser) => {
  var Profile = Parse.Object.extend("Profile");
  var profile = new Profile({
    "firstname" : "John",
    "lastname" : "Smith",
    "user" : newUser
  });
  return profile.save();
})

Unfortunately, this still involves making two separate requests to the Parse Server which is inefficient for a frontend to do; it is wise to avoid multiple-step client-server communication flows when possible.

Also, regarding security, the above code is putting the creation process in a clients hands which is never smart. We would like to prevent our data integrity from relying on a client properly completing all steps of a flow. They could, for example, send a custom request that creates a user with no profile, corrupting the app’s persistent data.

Why not do this all in one step using cloud code? It can prevent bloating of frontend code, and ensure that the client is not doing unnecessary / insecure work! Here’s what we want to do instead from the client for sign up

1
2
3
4
5
6
7
8
9
10
Parse.Cloud.run('signupUser',
{
  username: 'myname',
  password : "mypass",
  email : "[email protected]",
  firstname : "John",
  lastname : "Smith" }
).then(function(newuser) {

});

Parse also defines for beforeSave triggers, allowing the creation of the profile when the user signs up. However by using a function we may intuitively pass firstname and lastname attributes that the profile will use

Cloud code signup function

Lets get started! Move to your project directory that is synced with Back4App (see prereqs if you don’t know what this means). We will assume the following structure:

1
2
./cloud
./cloud/main.js

In our case, upon initialization, we chose ‘cloud’ as our Directory Name. Your directory can be called whatever you want.

The function (in main.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Parse.Cloud.define("signuserUp", function(request, response) {
  // Make sure the necessary parameters are passed first
  var params = request.params;
  if (!params.username || !params.email || !params.password || !params.firstname || !params.lastname)
      return response.error("Missing parameters: need username, email, password, firstname, & lastname");


  // Execute the signup flow
  var user = new Parse.User({
      username : params.username,
      password : params.password,
      email : params.email
    });
    user.signUp(null, {useMasterKey : true})
    .then((newUser) => {
      var Profile = Parse.Object.extend("Profile");
      var profile = new Profile({
        firstname : params.firstname,
        lastname : params.lastname,
        user : newUser
      })
      return profile.save(null, {useMasterKey : true});
    })
    .then((prof) => response.success(prof))
    .catch((e) => {
      response.error(e.message);
    })
});

You may notice the ‘useMasterKey’ option being passed; this allows the cloud code to supercede any Roles or ACLs that may be in place. Since the client doesn’t touch this code, there is no risk of them hijacking our server. However, please be careful with this flag!

In case it’s not obvious why this might be preferable to placing this functionality in the client code, here are some advantages:

  • Offloads computation to the server rather than the device
  • Explicitly defines the functionality for a process
  • Easier to create fail safe functions
  • Gives the client an intuitive interface
  • Prevents the possibility that a client will ‘half-do’ a process.

2.) Refactoring our directory structure

Great, we’ve created two cloud functions. We could obviously test these functions by running them and checking the Parse Dashboard, but that is not scalable or efficient. We instead want to create automated tests specifically for the methods that can be run continuously. So we will separate our code a little bit.

We will move the functions we created in main.js to a new file called cloud-functions.js (in the same directory). Then we will import these functions into main, and bind them to the Cloud Function definitions. The idea is to decouple the functions from the cloud interface so we may test them without inefficiently sending HTTP requests. This will make a lot of sense as we create the test suite.

create the functions file

1
2
3
4
# remember,  the 'cloud' directory was determined
# when we initialized the directory with our Back4App project
# check the Prerequisites if this does not make sense
touch ./cloud/cloud-functions.js

You may be aware that you can use ‘require’ in Node.js to pull in functions, objects, and variables from other files.

We will thus define functions corresponding with the Parse Cloud Function we created in Step 1.

One possibly confusing point is that the functions we are defining will be returning functions, which then can be hooked into the Parse Cloud definition. It may seem strange to use a function to return a function, but this will give us the power to swap out Parse Servers later on when we are writing our tests.

You may have noticed that you can use the Parse object in your Cloud Code, without ever having to define or import it. This is due to the server that runs this code adding Parse automatically. However, if we want to run tests on the functions locally, we are not afforded an instance. As a matter of fact, we would like to supply our own instance that corresponds to a test Parse server, where there is no harm in data being created or deleted.

So each function will accept ‘Parse’ as a parameter, and return the cloud functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// cloud-functions.js
module.exports.SignupUser = function(Parse) {
  return function (request, response) {
    // Copied from main.js:
    // Make sure the necessary parameters are passed first
    var params = request.params;
    if (!params.username || !params.email || !params.password || !params.firstname || !params.lastname)
        return response.error("Missing parameters: need username, email, password, firstname, & lastname");
    // Execute the signup flow
    var user = new Parse.User({
      username : params.username,
      password : params.password,
      email : params.email
    });
    user.signUp(null, {useMasterKey : true})
    .then((newUser) => {
      var Profile = Parse.Object.extend("Profile");
      var profile = new Profile({
        firstname : params.firstname,
        lastname : params.lastname,
        user : newUser
      })
      return profile.save(null, {useMasterKey : true});
    })
    .then((prof) => response.success(prof))
    .catch((e) => {
      response.error(e.message);
    })
  }
}

In main.js, remove everything from before. Import the Cloud Function, and bind the function to the Cloud Function definition like this:

1
2
3
4
5
// main.js
var cloudFunctions = require("./cloud-functions");
// Note that we are injecting the Parse instance, which is automatically supplied in the
// context of Parse Cloud Code, but not on local tests
Parse.Cloud.define("signuserUp", cloudFunctions.SignupUser(Parse));

Great! We have not changed the functionality at all since step 1, but we have decoupled the function from the Cloud Code. In the next step we will create a unit test!

3.) Create the test suite

For our test suite we will be using Jasmine, the popular testing framework. However, our code so far is completely agnostic of our tests, so you may use whatever framework or platform that you prefer.

Install the development dependencies

Let’s install Jasmine and Jasmine-node (an integration of Jasmine and our Node.js environment)

$ npm install jasmine jasmine-node --save-dev

Now let’s install two libraries our test suite will use. It will use the Parse SDK to connect to a fake Parse Server, and the events library for stubbing out the request object

$ npm install parse events --save-dev

Now, using the Jasmine utility, let’s initialize our test directory.

$ ./node_modules/jasmine/bin/jasmine.js init

If you prefer, you may install jasmine globally with $ npm install -g jasmine, then you can initialize with this $ jasmine init

This guide will assume you do not install Jasmine globally, though it is recommended. If you do, you may replace all instances of ‘/node_modules/jasmine/bin/jasmine.js’ with simply ‘jasmine’

This should create a directory called spec, which itself includes a support folder containing configuration information for Jasmine.

By default, Jasmine knows to look for files that end in the “.spec.js” extension, so we will name our tests accordingly.

Create the file for our first unit test:

$ # in the ./spec directory
$ touch signup-user.spec.js'

Add a utilities directory with two files that will help with our tests:

$ # in the ./spec directory
$ touch utils/purge-parse-table.js
$ touch utils/response-stub.js

Finally, create a constants file in the same directory. The utility for this file will be explained later

$ # in the ./spec directory
$ touch constants.js

Here’s what your directory should now look like:

├── cloud
│   ├── cloud-functions.js
│   ├── main.js
├── node_modules
├── spec
│   ├── support
│   │   ├── jasmine.json
│   ├── utils
│   │   ├── purge-parse-table.js
│   │   ├── response-stub.js
│   ├── signup-user.spec.js
│   ├── constants.js

4.) Swapping in a test Parse Server

Testing around Parse

Since our methods involve a Parse Server, we want to be able to test that interaction. There are two ways to do this:

A. We can “stub” out the Parse SDK object, by defining an object that implements the same interface. Then simply pass that object as the parameter to our cloud methods. That might look something like this.

1
2
3
4
5
6
7
8
9
10
var ParseStub = {
  // make sure all used methods  and properties are defined
  User : function () {
    // Constructor function
      this.set = function (key, val) {
        // logic here to implement the parse object set
      }
  }
}
signupUser(ParseStub); // returns cloud function that we can test

B. Another approach is set up a real Parse Server that will serve only for test data. This will involve the slow HTTP layer that Parse uses, but also allow us to test the data in the database. In our tests we’d need to import the Parse SDK, and configure it with a test server.

Posting Ap

The two places that can be stubbed when testing cloud code: A.) Stub a Parse SDK that won’t make HTTP requests, or B.) Swap in a test database implementation

Neither of these approaches is the “right” answer. It depends on what you’re trying to test. Stubbing out the interface for the Parse SDK (even just the parts we are using) is a lot of work. Additionally, we are going to test the persistence of data after saving in this example, so we will use the second approach.

Create a test Parse instance

Lets:

  • Create a test Parse Server on Back4App
  • Grab the Application ID, and Master Key and save them into our constants file
  • Initialize the Parse SDK in our spec file, so our test uses the test server

You are welcome to run a local Parse Server for your tests. We will simply create another Back4App application in our dashboard.

If you need a refresher on how to provision another Back4App server, head on over to the Create New App tutorial. Call your application whatever you want, though it might be wise to use something like TestBackend. Then just grab the Application ID and Master Key from Dashboard > App Settings > Security & Keys.

Now save these tokens in our constants file like this:

1
2
3
4
5
6
// ./spec/constants.js
// Paste in your app id and master key where the strings are
module.exports = {
  APPLICATION_ID : "PASTE YOUR APPLICATION KEY HERE",
  MASTER_KEY : "PASTE YOUR MASTER KEY HERE"
}

DO NOT put the Application ID and Master Key from your Production App!!! We will be deleting data, and doing so will risk you losing data

4.) Testing Utilities

Request/ Response

Cloud functions are passed as parameters the Express Request and Response objects.

The server automatically creates these parameters when they are run on the cloud, so for our test environments we must create doubles.

Request

This case is easier. When a cloud function is called, data is passed; in our case, the profile and user information is passed. Every argument thats provided is accessible from the request.params property.

So if we call a cloud function like

1
2
3
4
5
6
7
8
9
// client code, calling Parse function
Parse.Cloud.run('fakefunction',
{
  data1: 'I am data1',
  data2: {
    prop : "Nested property"
  }
}
);

Then the request.params property will contain the passed data:

1
2
3
4
5
6
7
8
// Server code, running the Parse function
  console.log(request.params);
// {
//   data1: 'I am data1',
//   data2: {
//     prop : "Nested property"
//   }
// }

Simple enough, for our tests, when calling our cloud function the first argument should be of the form

1
2
3
4
5
6
7
{
  params : {
    username: 'testuser',
    firstname: "John",
    // the rest of the arguments
  }
}

Thus we don’t need to create a special mock object in this case.

Response

The response object allows the cloud code to send an HTTP response to the client representing either a success or a failure. We would like to know which is called when invoking the cloud function. Below is a mock object that will allow our test to determine whether the invocation was successful or not. If this is confusing, don’t worry, just place it in your ./spec/utils/response-stub.js file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ./spec/utils/response-stub.js
const EventEmitter = require('events');
/**
 * Wrapper around response stub. Simplifies testing cloud functions that
 * employ a response parameter
 */
function ResponseStub () {
  this.responseListener = new EventEmitter();
  this.responseStub = {};
  /**
   * Success method that cloud functions expect
   */
  this.responseStub.success = (resp) => {
    this.responseListener.emit("success", resp);
  }
  /**
   * Error method that cloud functions expect
   */
  this.responseStub.error = (resp) => {
    this.responseListener.emit("error", resp);
  }
  /**
   * Listens for errors and successes from stub and returns Promise that resolves or rejects accordingly
   */
  this.resolver = new Promise((resolve, reject) => {
    this.responseListener.on("success", (resp) => resolve(resp));
    this.responseListener.on("error", (err) => reject(err));
  });
}

/**
 * reeturns stub to feed to cloud function
 */
ResponseStub.prototype.getStub = function () {
  return this.responseStub;
}

/**
 * returns promise that will indicate the success or failure
 */
ResponseStub.prototype.onComplete = function () {
  return this.resolver;
}

module.exports = ResponseStub;

In short, this javascript constructor function will provide a way for our test to pass in a response object which indicates by Promise resolution / rejection whether the Cloud Function would have returned a success or an error.

Cleaning up the database

We obviously don’t want our test Parse database to hold onto what is accumulated during a test. Lets define a utility for clearing database tables, that can be called prior to (or after) test cases.

Add the following to ‘spec/utils/purge-parse-table.js’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// spec/utils/purge-parse-table.js
/**
 * Removes all rows from the Parse Database
 * @param  {string} tablename the name of the parse table to be purged
 * @return {Promise}  promise to destroy each item  in the table
 */
module.exports = function (Parse) {
  return (tablename) => {
    var tableQuery;
    if (tablename === "User")
      tableQuery = new Parse.Query(Parse.User);
    else tableQuery = new Parse.Query(tablename);
    return tableQuery.find({useMasterKey : true}).then((items) => {
      var destroyQueue = [];
      for (var i=0; i<items.length; i++) {
        destroyQueue.push(items[i].destroy({useMasterKey : true}));
      }
      return Promise.all(destroyQueue).catch((e) => {console.log("Error destroying: " + e.message)});
    });
  }
}

After defining this function, it is a good time to remind you to make sure your spec/utils/constants.js is configured to your TEST Parse Application, NOT your Production Parse Application. This will delete data, so please confirm that this is the empty database you created above

This function accepts our configured Parse SDK, and returns another function. The returned function accepts a tablename, and removes all data from the corresponding Parse table.

Again, the idea of returning a function may seem strange, but it allows the test spec to configure the Parse endpoint, then reference a function that will clear THAT Parse endpoint’s table

Awesome! Now let’s write our test!

5.) Test that the Cloud Function will send an error if the proper parameters are not passed

The Cloud Function relies on certain parameters being included, and should fail if, for example, the ‘firstname’ was not sent. Let’s be sure.

We will be editing our test file (finally!) spec/signup-user.spec.js.

Here’s what needs to happen before the test definitions:

  • Import the Parse NodeJS SDK
  • Import our constants, and configure the Parse SDK to point at our test server
  • Import our Cloud Function
  • Import our “Purge Table” utility
  • Import the Response Mock Object we created

The following will do:

1
2
3
4
5
6
7
8
9
10
// Hook into your testing server
var Parse = require('parse/node');
var constants = require("./constants");
// head over to your Parse dash board for your test server, and grab your keys. Swap out the strings with the place holders below
Parse.initialize(constants.APPLICATION_KEY, null, constants.MASTER_KEY);
// if you are running a localhost Parse server, set the serverUrl accordingly
Parse.serverURL = 'https://parseapi.back4app.com'
var signupUser = require("../cloud/cloud-functions").SignupUser(Parse);
var purgeTable = require("./utils/purge-parse-table")(Parse);
var ResponseStub = require("./utils/response-stub");

Now lets add the test cases. The Jasmine introduction may help to understand the structure better, but it looks like this (taken from the intro):

describe("A suite", () => {
  it("contains spec with an expectation", () => {
    expect(true).toBe(true);
  });
});

So describe blocks encapsulate test suite’s, and the ‘it’ blocks represent cases and expectations.

By passing a parameter to the ‘it’ blocks, you may run tests asynchronously. The test won’t complete until the parameter is invoked like this:

it("will not finish until done is called", (done) => {
  setTimeout(() => {
    done();
  }, 100); // 100 ms
  expect(x).toBe(y);
})

This is helpful since one of our tests will use HTTP, thus should be run asynchronously in this manner as using HTTP is a non-blocking procedure in NodeJS.

Additionally, Jasmine allows for special blocks within suites that can run at different points in the testing lifecycle. We want to delete all tables before each test, so we will execute purging code in the beforeEach block.

Enough talking, lets add some code! Place the code below into your spec/signup-user.spec.js, below the imports we already added:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//spec/signup-user.spec.js
// imports above
describe("SignupUser", ()=> {
  beforeEach((done) => {
    /// purge the user and profile tables, and then proceed
    Promise.all([purgeTable("User"), purgeTable("Profile")])
    .catch((e) => fail(e))
    .then(() => done());
  });
  it ("should reject a request to signup that does not contain all the parameters", (done) => {
    var responseStub = new ResponseStub();
    responseStub.onComplete()
    .then(() => fail("Should have failed due to invalid parameters"))
    .catch((e) => {})
    .then(() => done());

    signupUser({ params : {}}, responseStub.getStub());

  });
});

Awesome, our first test is under our belts. In the beforeEach block, we purge the User and Profile tables. Then the first test-case is triggered. It verifies that passing invalid parameters to the signupUser function causes the function to send an error.

It uses the response stub to make sure the function ultimately rejects. Because ‘signupUser’ will fail, the initial ‘then’ block on the stub should not be invoked. If it is, then our test fails!

Go ahead and run the test using the following:

$ ./node_modules/jasmine/bin/jasmine.js spec/signup-user.spec.js

You should see the following output:

Randomized with seed 24618
Started
..


1 specs, 0 failures
Finished in 1.376 seconds
Randomized with seed 24618 (jasmine --random=true --seed=24618)

6.) A Test On Data Persistence

Hope you have one more test in you! We are going to verify that when our Cloud Function runs properly, our database will be as expected: a Profile will exist, with a reference to a User object, both with the expected attributes.

Add the following block to our existing ‘describe’ suite block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//spec/signup-user.spec.js
// inside describe
it ("should signup a User, and also create a Profile that contains a reference to the user", (done) => {
  var responseStub = new ResponseStub();
  var stub = responseStub.getStub();
  signupUser({
      params : {
        firstname : "John",
        lastname : "Smith",
        email : "[email protected]",
        username : "jsmith1",
        password : "SecretCatchphrase1"
      },
    },
    stub
  );
  responseStub.onComplete()
  .then((resp) => {
    var profileQ =  new Parse.Query("Profile");
    profileQ.equalTo("lastname", "Smith");
    return profileQ.find({useMasterKey : true});
  })
  // Check to make sure the profile we retrieve is valid
  .then((profiles) => {
    if (profiles.length === 0) throw new Error("No profile's found");
    expect(profiles[0].get('firstname')).toBe("John");
    // get the corresponding user
    return profiles[0].get("user").fetch({useMasterKey : true})
  })
  // Check to make sure the user is what we expect
  .then((user) => {
    expect(user.getUsername()).toBe("jsmith1");
  })
  .catch((e) => {
    console.log(e)
    fail(e);
  })
  .then(() => done());
});

Ok this is a lot, so lets step through what occurs.

We instantiate a response mock object, as in the first test-case. Then we run signupUser with a request double containing valid parameters, as well as the response mock (lines 6-16).

Next, this code listens for the mock object’s onComplete method, which will return a Promise. The Promise will reject if response.error was called, and resolve if response.success was called. Any rejections will cause the chain of Promises to skip to the catch block. Therefore, the fail method is placed in the catch block, as the test should fail if the Promise rejects.

The response of the Promise should resolve to the profile object. Once it resolves, we will query for a profile of the same last name as we created (lines 19-21). Then the test confirms that the ‘firstname’ of the profile is the same one that we passed (lines 25-26).

The next block fetches the user object associated with the profile. Parse object pointers fetch separately, hence the need for another Promise block.

Finally, the code confirms that the corresponding user has the username that was passed to the signupUser function. Then the test finishes.

Go ahead and run the suite one more time: Go ahead and run the test using the following:

$ ./node_modules/jasmine/bin/jasmine.js spec/signup-user.spec.js

You should see the following output:

Randomized with seed 24618
Started
..


2 specs, 0 failures
Finished in 2.876 seconds
Randomized with seed 24618 (jasmine --random=true --seed=24618)

Awesome! we wrote some cloud code, and integrated a testing framework.

Conclusion

If you got lost at all, or merely want the code for this example head over to the Github Repo. Follow the instructions to download and run.

If something is not clear, or doesn’t work, please reach out to me via my gmail, jackconsidine3.

I hope you enjoyed this tutorial, and gained some insight!