Advanced Guides
Integrated Tests
36 min
how to integrate tests into your parse cloud code functions introduction this is a guide written by https //github com/considine , our guest writer and lead developer at https //koptional com/ 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 https //github com/back4app/template cloud code unit test 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 https //en wikipedia org/wiki/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 at back4app follow the create new app tutorial to learn how to create an app at back4app back4app command line configured with the project follow the setting up cloud code tutorial to learn how to set up cloud code for a project npm installed on your command line note this library will use the https //www promisejs org/ , which shouldn’t be too complicated let’s create a basic social media backend 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 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 1 var user = new parse user(); 2 user set("username", "my name"); 3 user set("password", "my pass"); 4 user set("email", "email\@example com"); in our case, we would like to also initialize a profile, and point it at the user object parse server 3 x 1 try { 2 await user signup(null, {usemasterkey true}); 3 let profile = parse object extend("profile"); 4 let profile = new profile({ 5 firstname params firstname, 6 lastname params lastname, 7 user user 8 }) 9 return profile save(null, {usemasterkey true}); 10 } catch (err){ 11 return (err message); 12 } parse server 2 x 1 user signup(null, { 2 success function (newuser) { 3 var profile = parse object extend("profile"); 4 var profile = new profile(); 5 profile set("firstname", "john"); 6 profile set("lastname", "smith"); 7 profile set("user", newuser); 8 profile save(); 9 }, 10 error function (err) { 11 // handle error 12 } 13 }) you could shorten that syntax to be something like this parse server 3 x 1 let user = new parse user({ 2 username params username, 3 password params password, 4 email params email 5 }); 6 7 try { 8 await user signup(null, {usemasterkey true}); 9 let profile = parse object extend("profile"); 10 let profile = new profile({ 11 firstname params firstname, 12 lastname params lastname, 13 user user 14 }) 15 return profile save(null, {usemasterkey true}); 16 } catch (err){ 17 return (err message); 18 } parse server 2 x 1 var user = new parse user({ 2 username params username, 3 password params password, 4 email params email 5 }); 6 user signup(null) 7 then((newuser) => { 8 var profile = parse object extend("profile"); 9 var profile = new profile({ 10 "firstname" "john", 11 "lastname" "smith", 12 "user" newuser 13 }); 14 return profile save(); 15 }) 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 1 parse cloud run('signupuser', 2 { 3 username 'myname', 4 password "mypass", 5 email "email\@example com", 6 firstname "john", 7 lastname "smith" } 8 ) then(function(newuser) { 9 10 }); parse also defines for http //docs parseplatform org/cloudcode/guide/#beforesave triggers 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 parse server 3 x main js 1 parse cloud define("signuserup", async(request) => { 2 // make sure the necessary parameters are passed first 3 let params = request params; 4 if (!params username || !params email || !params password || !params firstname || !params lastname) 5 throw new error("missing parameters need username, email, password, firstname, & lastname"); 6 7 // execute the signup flow 8 let user = new parse user({ 9 username params username, 10 password params password, 11 email params email 12 }); 13 14 try { 15 await user signup(null, {usemasterkey true}); 16 let profile = parse object extend("profile"); 17 let profile = new profile({ 18 firstname params firstname, 19 lastname params lastname, 20 user user 21 }) 22 return profile save(null, {usemasterkey true}); 23 } catch (err){ 24 return (err message); 25 } 26 }); parse server 2 x main js 1 parse cloud define("signuserup", function(request, response) { 2 // make sure the necessary parameters are passed first 3 var params = request params; 4 if (!params username || !params email || !params password || !params firstname || !params lastname) 5 return response error("missing parameters need username, email, password, firstname, & lastname"); 6 7 // execute the signup flow 8 var user = new parse user({ 9 username params username, 10 password params password, 11 email params email 12 }); 13 user signup(null, {usemasterkey true}) 14 then((newuser) => { 15 var profile = parse object extend("profile"); 16 var profile = new profile({ 17 firstname params firstname, 18 lastname params lastname, 19 user newuser 20 }) 21 return profile save(null, {usemasterkey true}); 22 }) 23 then((prof) => response success(prof)) 24 catch((e) => { 25 response error(e message); 26 }) 27 }); 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 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 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 parse server 3 x cloud functions js 1 // cloud functions js 2 module exports signupuser = function(parse) { 3 return async(request) => { 4 // copied from main js 5 // make sure the necessary parameters are passed first 6 let params = request params; 7 if (!params username || !params email || !params password || !params firstname || !params lastname) 8 throw new error("missing parameters need username, email, password, firstname, & lastname"); 9 10 // execute the signup flow 11 let user = new parse user({ 12 username params username, 13 password params password, 14 email params email 15 }); 16 17 try { 18 await user signup(null, {usemasterkey true}); 19 let profile = parse object extend("profile"); 20 let profile = new profile({ 21 firstname params firstname, 22 lastname params lastname, 23 user user 24 }) 25 return profile save(null, {usemasterkey true}); 26 } catch (err){ 27 return (err message); 28 } 29 } 30 } parse server 2 x cloud functions js 1 // cloud functions js 2 module exports signupuser = function(parse) { 3 return function (request, response) { 4 // copied from main js 5 // make sure the necessary parameters are passed first 6 var params = request params; 7 if (!params username || !params email || !params password || !params firstname || !params lastname) 8 return response error("missing parameters need username, email, password, firstname, & lastname"); 9 // execute the signup flow 10 var user = new parse user({ 11 username params username, 12 password params password, 13 email params email 14 }); 15 user signup(null, {usemasterkey true}) 16 then((newuser) => { 17 var profile = parse object extend("profile"); 18 var profile = new profile({ 19 firstname params firstname, 20 lastname params lastname, 21 user newuser 22 }) 23 return profile save(null, {usemasterkey true}); 24 }) 25 then((prof) => response success(prof)) 26 catch((e) => { 27 response error(e message); 28 }) 29 } 30 } in main js, remove everything from before import the cloud function, and bind the function to the cloud function definition like this 1 // main js 2 var cloudfunctions = require(" /cloud functions"); 3 // note that we are injecting the parse instance, which is automatically supplied in the 4 // context of parse cloud code, but not on local tests 5 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 https //jasmine github io/ , 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 $ npm install g jasmine , then you can initialize with this $ jasmine init $ 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 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 var parsestub = { 2 // make sure all used methods and properties are defined 3 user function () { 4 // constructor function 5 this set = function (key, val) { 6 // logic here to implement the parse object set 7 } 8 } 9 } 10 signupuser(parsestub); // returns cloud function that we can test 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 https //github com/parse community/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 https //www back4app com/docs/get started/new parse app 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 // /spec/constants js 2 // paste in your app id and master key where the strings are 3 module exports = { 4 application id "paste your application key here", 5 master key "paste your master key here" 6 } 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 5\ testing utilities 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 1 // client code, calling parse function 2 parse cloud run('fakefunction', 3 { 4 data1 'i am data1', 5 data2 { 6 prop "nested property" 7 } 8 } 9 ); then the request params property will contain the passed data 1 // server code, running the parse function 2 console log(request params); 3 // { 4 // data1 'i am data1', 5 // data2 { 6 // prop "nested property" 7 // } 8 // } simple enough, for our tests, when calling our cloud function the first argument should be of the form 1 { 2 params { 3 username 'testuser', 4 firstname "john", 5 // the rest of the arguments 6 } 7 } 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 https //msdn microsoft com/en us/library/ff650441 aspx 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 // /spec/utils/response stub js 2 const eventemitter = require('events'); 3 / 4 wrapper around response stub simplifies testing cloud functions that 5 employ a response parameter 6 / 7 function responsestub () { 8 this responselistener = new eventemitter(); 9 this responsestub = {}; 10 / 11 success method that cloud functions expect 12 / 13 this responsestub success = (resp) => { 14 this responselistener emit("success", resp); 15 } 16 / 17 error method that cloud functions expect 18 / 19 this responsestub error = (resp) => { 20 this responselistener emit("error", resp); 21 } 22 / 23 listens for errors and successes from stub and returns promise that resolves or rejects accordingly 24 / 25 this resolver = new promise((resolve, reject) => { 26 this responselistener on("success", (resp) => resolve(resp)); 27 this responselistener on("error", (err) => reject(err)); 28 }); 29 } 30 31 / 32 reeturns stub to feed to cloud function 33 / 34 responsestub prototype getstub = function () { 35 return this responsestub; 36 } 37 38 / 39 returns promise that will indicate the success or failure 40 / 41 responsestub prototype oncomplete = function () { 42 return this resolver; 43 } 44 45 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 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’ 1 // spec/utils/purge parse table js 2 / 3 removes all rows from the parse database 4 @param {string} tablename the name of the parse table to be purged 5 @return {promise} promise to destroy each item in the table 6 / 7 module exports = function (parse) { 8 return (tablename) => { 9 var tablequery; 10 if (tablename === "user") 11 tablequery = new parse query(parse user); 12 else tablequery = new parse query(tablename); 13 return tablequery find({usemasterkey true}) then((items) => { 14 var destroyqueue = \[]; 15 for (var i=0; i\<items length; i++) { 16 destroyqueue push(items\[i] destroy({usemasterkey true})); 17 } 18 return promise all(destroyqueue) catch((e) => {console log("error destroying " + e message)}); 19 }); 20 } 21 } 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! 6\ 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 // hook into your testing server 2 var parse = require('parse/node'); 3 var constants = require(" /constants"); 4 // head over to your parse dash board for your test server, and grab your keys swap out the strings with the place holders below 5 parse initialize(constants application key, null, constants master key); 6 // if you are running a localhost parse server, set the serverurl accordingly 7 parse serverurl = 'https //parseapi back4app com' 8 var signupuser = require(" /cloud/cloud functions") signupuser(parse); 9 var purgetable = require(" /utils/purge parse table")(parse); 10 var responsestub = require(" /utils/response stub"); now let's add the test cases the jasmine introduction https //jasmine github io/2 1/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 1 //spec/signup user spec js 2 // imports above 3 describe("signupuser", ()=> { 4 beforeeach((done) => { 5 /// purge the user and profile tables, and then proceed 6 promise all(\[purgetable("user"), purgetable("profile")]) 7 catch((e) => fail(e)) 8 then(() => done()); 9 }); 10 it ("should reject a request to signup that does not contain all the parameters", (done) => { 11 var responsestub = new responsestub(); 12 responsestub oncomplete() 13 then(() => fail("should have failed due to invalid parameters")) 14 catch((e) => {}) 15 then(() => done()); 16 17 signupuser({ params {}}, responsestub getstub()); 18 19 }); 20 }); 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 7\ 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 //spec/signup user spec js 2 // inside describe 3 it ("should signup a user, and also create a profile that contains a reference to the user", (done) => { 4 var responsestub = new responsestub(); 5 var stub = responsestub getstub(); 6 signupuser({ 7 params { 8 firstname "john", 9 lastname "smith", 10 email "jsmith\@example com", 11 username "jsmith1", 12 password "secretcatchphrase1" 13 }, 14 }, 15 stub 16 ); 17 responsestub oncomplete() 18 then((resp) => { 19 var profileq = new parse query("profile"); 20 profileq equalto("lastname", "smith"); 21 return profileq find({usemasterkey true}); 22 }) 23 // check to make sure the profile we retrieve is valid 24 then((profiles) => { 25 if (profiles length === 0) throw new error("no profile's found"); 26 expect(profiles\[0] get('firstname')) tobe("john"); 27 // get the corresponding user 28 return profiles\[0] get("user") fetch({usemasterkey true}) 29 }) 30 // check to make sure the user is what we expect 31 then((user) => { 32 expect(user getusername()) tobe("jsmith1"); 33 }) 34 catch((e) => { 35 console log(e) 36 fail(e); 37 }) 38 then(() => done()); 39 }); 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 conclusion if you got lost at all, or merely want the code for this example head over to the github repo https //github com/back4app/template cloud code unit test 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!