Integrated Tests
This is a guide written by John Considine, our guest writer and lead developer at K-Optional. 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.
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).
To complete this tutorial, you need:
- An app 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
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!
This assumes you have a Back4App project created, and the command line tool installed (see prerequisites).
For examples of frontend code, this guide will refer 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
In our case, we would like to also initialize a Profile, and point it at the User object.
You could shorten that syntax to be something like this:
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 client's 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
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
Let's 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:
In our case, upon initialization, we chose ‘cloud’ as our Directory Name. Your directory can be called whatever you want.
You may notice the ‘useMasterKey’ option being passed; this allows the cloud code to supersede 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 of a process
- Easier to create fail-safe functions
- Gives the client an intuitive interface
- This prevents the possibility that a client will ‘half-do’ a process.
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
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.
In main.js, remove everything from before. Import the Cloud Function, and bind the function to the Cloud Function definition like this:
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!
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.
Let’s install Jasmine and Jasmine-node (an integration of Jasmine and our Node.js environment)
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
Now, using the Jasmine utility, let’s initialize our test directory.
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:
Add a utilities directory with two files that will help with our tests:
Finally, create a constants file in the same directory. The utility for this file will be explained later
Here’s what your directory should now look like:
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.
B. Another approach is to 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.
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.
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:
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
Cloud functions are passed as parameters in 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.
This case is easier. When a cloud function is called, data is passed; in our case, the profile and user information are passed. Every argument that is provided is accessible from the request.params property.
So if we call a cloud function like
Then the request.params property will contain the passed data:
Simple enough, for our tests, when calling our cloud function the first argument should be of the form
Thus we don’t need to create a special mock object in this case.
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 what 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.
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. Let's 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’:
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, and then reference a function that will clear THAT Parse endpoint’s table
Awesome! Now let’s write our test!
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:
Now let's add the test cases. The Jasmine introduction may help to understand the structure better, but it looks like this (taken from the intro):
So describe blocks encapsulate test suites, 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:
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 the purging code in the beforeEach block.
Enough talking, let's add some code! Place the code below into your spec/signup-user.spec.js, below the imports we already added:
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 rejected. 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:
You should see the following output:
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:
Ok this is a lot, so let's 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 a response.error was called, and resolve if a 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:
You should see the following output:
Awesome! we wrote some cloud code, and integrated a testing framework.
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!