Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit testing of Cloud Code functions #2488

Closed
drbarto opened this issue Aug 9, 2016 · 30 comments
Closed

Unit testing of Cloud Code functions #2488

drbarto opened this issue Aug 9, 2016 · 30 comments

Comments

@drbarto
Copy link

drbarto commented Aug 9, 2016

Is there a recommended way to unit-test Cloud Code functions? I couldn't find any libs or best practices that take into account the self-hosting capabilities of parse-server, and my related question on StackOverflow was ignored.

Any info on that topic from the Parse community would be appreciated!

@gnz00
Copy link

gnz00 commented Aug 9, 2016

Register the hooks pointing to a local version via ngrok and then invoke the tests.

You can use the scripts here: https://github.com/ParsePlatform/CloudCode-Express
Here is how I invoke: lib/scripts/record-webhooks && parse configure hooks -b $HOOKS_URL webhooks.json

@acinader
Copy link
Contributor

I've been using https://github.com/HustleInc/parse-mockdb and it works well. I'm going to try @maysale01 suggestion though too. I'll report back on which I prefer and why (not for a couple of days though).

@chrismatheson
Copy link

I found this during my search for general info on unit testing Parse applications. My plan for cloud code is essentially to extract the functions out and unit test them with fake request / response objects.

i.e. instead of

main.js

Parse.Cloud.define("doThing", function (request, response) {

split into two files

doThing.js

module.exports = function (request, response) { });

main.js

Parse.Cloud.define("doThing", require('./doThing'));

Then doThing can be tested in a spec file something like

var route = require('./doThing');

var fakeReqest = {
 .... stuff
}

var fakeResponse = {
  success: stub(),
  error: stub()
}

route(fakeReq, fakeRes)

expect(fakeRes.success).toHaveBeenCalled();

however the stumbling block I've met is not so much the "calling" of a function to test it, but the amount of things that require mocking / stubbing. Parse.Query Parse.ACL etc that will most likely be used in a cloud function. This extends to front end code as well, and there does not seem to be a standard approach for this, or at least that i can find myself :)

Its the nature of using a BaaS or any third party service that large areas of functionality need to be mocked / stubbed, however i have to admire the approach of Angular, in providing sort of pre-done stubs / helpers for this.

FWIW id personally not recommend the ngrok web hook approach for unit tests the amount of moving parts there seems a bit high to me personally. That approach seems more for E2E testing

@acinader
Copy link
Contributor

@chrismatheson I do more or less what you're describing using @TylerBrock's https://github.com/HustleInc/parse-mockdb

Works like a charm.

@drbarto
Copy link
Author

drbarto commented Aug 18, 2016

What are the benefits of using parse-mockdb instead of parse-server? The "real" server can be run as an npm module as well, so I don't really get why one should use an incomplete re-implementation of the API.

In my current test setup, I start a (local) Parse server before running the tests and have my tests communicate with the real Cloud Code functions. The only downside I see is that for a really clean test run, I would have to reset the mongodb (which in my setup runs as a normal background process on the system). I find this both easier and more realistic than mocking various layers. But I would be interested in the opinion of others on that topic.

@acinader
Copy link
Contributor

i like that with the parse-mockdb i don't have to have a mongo service running. i've tinkered a little with running parse-server for unit tests and have been looking at the parse-server unit tests, but the setup is onerous to me so far. will try again at some point (soon probably) but in the meantime, the mock setup was reasonably easy to get going and has worked well.

@flovilmart
Copy link
Contributor

Seems extensively answered, closing

@majidhassan
Copy link

I'm also looking for a tool to test my cloud-code, jobs (using agenda) as well as save/delete hooks.

I started with python-nosetests mainly to test parse-server functionality at the earliest stages of the parse-server release. I'm not sure whether using nosetests in that context is the best idea, though, but it's the testing tool I'm most familiar with, and it started off as a very basic testcase.

The nosetests make calls to the actual API - I have a test environment setup just for that - instead of using a mock database or API. In my opinion, that's the whole point of testing.

Currently I'm looking into expanding my tests to achieve a higher coverage, but I'm not sure if I should stick to nosetests, or if there's a better way/tool to tackle this.

@flovilmart
Copy link
Contributor

flovilmart commented Oct 13, 2016

You should test your jobs the same way you test any express endpoint. Cloud code supports require properly, so you don't have to define your jobs as:

Parse.Cloud.job('name', function(req, res) {
// do the job
});

You can very well have:

// lib/jobs.js

module.exports = {
   job_1: function() {},
   job_2: function() {} 
}
...

// main.js

var jobs = require('lib/jobs');
Object.keys(jobs).forEach(function(jobName) {
   Parse.Cloud.job(jobName, jobs[jobName]);
});

Then you could use requir('lib/jobs') in your unit tests.

@majidhassan
Copy link

@flovilmart Good idea! Didn't think about that.

Any ideas regarding the testing tool itself. Nosetests currently do the job, but I'd like to know, if there's a better way to test parse (or node in general) specific code.

What do you use for testing parse-server?

@acinader
Copy link
Contributor

@majidhassan parse-server uses jasmine for unit tests. I've found that looking at how parse-server does unit tests to be useful in designing my own unit tests.

@drbarto
Copy link
Author

drbarto commented Oct 13, 2016

I use gulp to run tests and mocha to write them. Mocha is great for Parse because it has good support for promises and testing async code (haven't used jasmine but it seems to be equally well suited); however, I think the test framework itself is the least interesting aspect of a Parse test setup. Here are some ideas which helped me to create a testing environment:

I use a local Parse server and mongodb to avoid cluttering my public hosted services with test data. After overcoming the initial setup hurdle, this works like a charm.

In my tests, I call CC functions via the Parse API (Parse.Cloud.run) instead of directly invoking a JavaScript function. While this requires a bit more test setup (e.g. fist create a dummy user, then call a CC function which depends on the req.user argument), it covers the entire lifecycle of a function call.

I run both CC and tests in the same process: the Parse server is started as a "before test" action and stopped as an "after test" action. This allows me to debug (e.g. set breakpoints) the test code as well as the server-side code, which has helped me tremendously in finding bugs.

The only drawbacks I have encountered with this setup is that a) it depends on a running mongodb instance, which I have to manually reset (haven't found a good way for automating that yet), and b) the mixed log output of test runner and server code can get a bit confusing.

@majidhassan
Copy link

@drbarto I have a similar setup, while using python-nosetests as for testing. Is it possible to get any code coverage using gulp + mocha?

Also, I have - in a way - automated the process of setting up a parse-server for testing. Checkout my script here. Disclaimer: May contain bugs!

@drbarto
Copy link
Author

drbarto commented Oct 15, 2016

@majidhassan I haven't used code coverage with my Cloud Code tests so far, but just gave it a try with a tool called istanbul, which frankly I find awesome; it worked out of the box without any additional config steps, as described here, and gave me a nice line-by-line coverage report.

Your install script seems very comprehensive, I'll definitely have a look into it. For my test environment, I so far just use the parse-server node package which gets configured, started and stopped as part of the test lifecycle. This way I can run the server inside Visual Studio Code, which is the best Node dev&debug environment I have found so far. Nothing beats setting breakpoints in Cloud Code functions.

@majidhassan
Copy link

@drbarto I've been trying to make some API tests work using gulp-mocha and gulp-istanbul, but I can't seem to get any coverage. I've posted an issue here. Maybe you could take a look and let me know, if anything is obviously wrong with my code.

I could post my code here as well, but I'm thinking this is more of an Istanbul, than a parse issue, so it shouldn't be here. Let me know, what you think.

@drbarto
Copy link
Author

drbarto commented Oct 20, 2016

It looks like that you start your Parse server as a separate process and the unit test depends on having a running Parse server present. From my understanding, coverage checking can not work this way. Only when Cloud Code and the tests run in the same process, istanbul is able to observe which code paths are taken.

Try to start your Parse server as part of the test setup:

// define your server options, mount path and port, then...
function startServer() {
  return new Promise(function(resolve, reject) {
    const api = new ParseServer(options);
    const app = express();
    app.use(mountPath, api);

    this.httpServer = require('http').createServer(app);
    this.httpServer.listen(port, function(error) {
      if (error) {
        reject(error);
      }
      else {
        resolve();
      }
    });
  });
}

Here is a simple test file which starts a server and initializes the Parse client in the beforeEach hook; then it runs a Cloud Code function using the Parse client SDK:

const chai = require('chai');
const expect = chai.expect;
const chaiAsPromised = require("chai-as-promised");
const Parse = require('parse/node');
chai.use(chaiAsPromised);

const Parse = require('parse/node');
describe('my test', function() {
  beforeEach(function() {
    return startServer().then(function() { 
      Parse.initialize(appId, clientKey, masterKey);
      Parse.serverURL = 'http://localhost:1337/parse';
    });
  }

  afterEach(function() {
    return Promise.resolve().then(function() { return stopServer() });
  }

  it('should do stuff', function(done) {
    // bridging between JS promises required by mocha/chai
    Promise.resolve()
      .then(function() {
        return Parse.Cloud.run('doStuff');
      }))
      .then(function(result) {
        expect(result).to.equal('stuff');
        done();
      })
      .catch(done); // will fail the test when CC returns an error
}

Finally, the gulp file for running the test with code coverage is very straightforward:

const gulp = require('gulp');
const mocha = require('gulp-mocha');
const istanbul = require('gulp-istanbul');

gulp.task('pre-test', function () {
  return gulp.src(['cloud/**/*.js'])
    .pipe(istanbul())
    .pipe(istanbul.hookRequire());
});

gulp.task('run-test', ['pre-test'], function() {
  return gulp.src('test/**/*.js', { read: false })
    .pipe(mocha({ reporter: 'spec' }))
    .pipe(istanbul.writeReports();
});

@majidhassan
Copy link

@drbarto Thanks for the comprehensive explanation. I followed these steps but I'm still getting 0% coverage from istanbul. Any ideas, what I might be missing?

screen shot 2016-10-20 at 5 30 17 pm

This is my test file:

const chai = require('chai');
const chai_as_promised = require("chai-as-promised");
const expect = chai.expect;
const express = require('express');
const http = require('http');
const parse_server = require('parse-server').ParseServer;
const path = require('path');

const Parse = require('/usr/lib/node_modules/parse-server/node_modules/parse/node');

const app_id = "app_id";
const database_uri = "mongodb://";
const master_key = "masterkey";
const server_url = "http://localhost:1337/parse";

const headers = {"X-Parse-Application-Id": app_id, "Content-Type": "application/json"};

chai.use(chai_as_promised);

function startServer() {
    return new Promise(function(resolve, reject) {
        const api = new parse_server({
            databaseURI: database_uri,
            cloud: "/home/parse/cloud/main.js",
            appId: app_id,
            masterKey: master_key,
            serverURL: server_url
        });

        const app = express();

        // Serve static assets from the /public folder
        app.use('/public', express.static(path.join(__dirname, '/public')));

        // Serve the Parse API on the /parse URL prefix
        app.use('/parse', api);

        var httpServer = http.createServer(app);
        httpServer.listen(1337, function(error) {
            if (error) {
                reject();
            }
            else {
                resolve(httpServer);
            }
        });
    });
}

function stopServer(httpServer) {
    httpServer.close(function() {
        console.log("Stopping server.");
    });
}

// UNIT test begin
describe('Parse Test', function() {
    var server;

    beforeEach(function() {
        return startServer().then(function(httpServer) { 
            Parse.initialize(app_id, "", master_key);
            Parse.serverURL = 'http://localhost:1337/parse';
            server = httpServer;
        });
    });

    afterEach(function() {
        return Promise.resolve().then(function() {
            return stopServer(server) 
        });
    });

    it('should get constants', function(done) {
        // bridging between JS promises required by mocha/chai
        Promise.resolve()
        .then(function() {
            return Parse.Cloud.run('getConstants');
        })
        .then(function(result) {
            expect(result["cover_height"]).to.equal(400);
            done();
        })
        .catch(done); // will fail the test when CC returns an error
    });
});

And the gulpfile:

var gulp = require('gulp'),
    mocha = require('gulp-mocha'),
    istanbul = require('gulp-istanbul'),

    JS_PATH_SERVER = "/home/parse/cloud_dir/cloud/",
    TEST_PATH_SERVER = "./tests/";

function handleError(err) {
  console.error(err);
}

gulp.task('pre-test', function () {
  return gulp.src([JS_PATH_SERVER + '**/*.js'])
    .pipe(istanbul({includeUntested: true}))
    .pipe(istanbul.hookRequire());
});

gulp.task('run-test', ['pre-test'], function() {
  return gulp.src(TEST_PATH_SERVER + '**/*.js', {read: false})
    .pipe(mocha({reporter: 'spec'}))
    .on("error", handleError)
    .pipe(istanbul.writeReports());
});

@drbarto
Copy link
Author

drbarto commented Oct 20, 2016

Looks like you use different Cloud Code root directories in the test and in the gulpfile... you start your server with cloud=/home/parse/cloud/main.js, but then istanbul is configured with /home/parse/cloud_dir/cloud/. If these two locations contain different code bases, the reason for your zero coverage is that istanbul observes different code than which is being actually run.

Btw -- I tried your code and after adding my server's config it worked without a problem.

@majidhassan
Copy link

@drbarto They're both the same directory. /home/parse/cloud is symlinked to /home/parse/cloud_dir/cloud. But just in case istanbul didn't like that I pointed them both to the original directory /home/parse/cloud_dir/cloud, which still didn't work.

Could you show me your server config if you're configuring it differently?

Also, are you installing the packages locally or globally? I installed them using this command:

npm install gulp gulp-mocha gulp-istanbul chai chai-as-promised express parse-server

Really doubt there's a wrong way to install them, but just in case there's something more to it. FYI I'm using Ubuntu 14.04.5 LTS on a Droplet on DigitalOcean.

@drbarto
Copy link
Author

drbarto commented Oct 21, 2016

As I said, your code worked without modifications (I just changed the hard-coded settings like paths to cloud code, mongo url etc.).

I installed the node packages locally (npm i --save-dev ...) but I don't think that this could make any difference.

However, I run my test setup locally (that is, on my dev machine) and not on a remote host. Not sure if this could cause any problems... you can give it a try though, just install mongodb on your dev machine and adapt the test script accordingly. (Running the tests locally has the additional benefit of debugging, so IMO it's definitely worth the setup work.)

@majidhassan
Copy link

@drbarto I'm not sure why it's still not running on my environment. I guess I'll have to test it locally, since this is currently the only difference. Although, it shouldn't differ whether it's tested locally or remotely, since when run remotely everything is run on the same environment.

Just to eliminate any variables, I'm running the test using gulp run-test. Is there a different way to do it?

The cloud code is under /home/parse/cloud_dir/cloud.
The gulp installation and the gulpfile.js are under /home/parse/tests.
The mocha tests are under /home/parse/tests/tests.

@drbarto
Copy link
Author

drbarto commented Oct 23, 2016

Looks all good to me.. no idea what's blocking you, sorry.

PS: Of course it should work on a remote server, I eventually plan to integrate the test code in a CI environment..

@majidhassan
Copy link

@drbarto Just for the record, it turned out it was an issue with one of the libraries I was using in my cloud code.

@houmie
Copy link

houmie commented May 15, 2017

@drbarto Martin, have you managed to find a way to drop the database in the teardown() of the unit test? I'm trying to achieve the same, and am stuck. I'm using swift on iOS. Worst case scenario I could get hold of my ids and class names and delete them after each test. But that's messy. Another problem is that deletion is async and could not finish in time before the next unit test has run, and hence the db won't be in a clean state. :(

@drbarto
Copy link
Author

drbarto commented May 15, 2017

@houmie Nope, in my current setup I just leave the test data in the database. Occasionally I'll manually reset the test db (i.e. delete it and bootstrap a new db just with the required class schema). I plan to automate this process but currently can live with the manual step because I have to do it pretty rarely.

In a way, not clearing the db after every test has a nice side effect: it ensures that my tests are independent of any pre-existing data and work stand-alone, no matter whether the db is fresh or crammed with thousands of test data sets.

Concerning your issue with async deleting in tearDown, you can synchronize this pretty easily with semaphores (this sample uses pre-Swift3 syntax but that should be easy to upgrade).

@houmie
Copy link

houmie commented May 18, 2017

@drbarto That was a great tip. So far it seems to be working nicely. Many Thanks.

@agordeev
Copy link

@drbarto Thanks for your tips, they are very helpful.

While this requires a bit more test setup (e.g. fist create a dummy user, then call a CC function which depends on the req.user argument)

How do you pass a user to the function? Passing this dictionary {user: user} after logging in doesn't work, returning an error: Parse Objects not allowed here.

@natanrolnik
Copy link
Contributor

natanrolnik commented Jun 20, 2017

@agordeev just make the request as the user, by passing the session token of that user into the options param of that function (the last one, the same one where you might add a useMasterKey if you need to):

let someUser = ... //Your user
Parse.Cloud.run("myFunction", {"param1": 1, "param2": anotherValue}, { sessionToken: someUser.getSessionToken() }).then...

This way, checking for req.user inside your function will give you someUser.

Check the docs of Parse.Cloud.run

@agordeev
Copy link

@natanrolnik Thanks Natan, that worked. Unfortunately, Parse.Cloud.run docs don't cover it, only stating we can pass success and error functions as options.

@byghuutran-zz
Copy link

onebody please help me find the error on following code, my backend developer are quit, i can't test the cloud code
system log:
`�[31merror�[39m: Invalid function: "weekChanged" code=141, message=Invalid function: "weekChanged"

Fri May 25 2018 10:18:07 GMT+0000 (UTC)
�[31merror�[39m: Error handling request: ParseError { code: 141, message: 'Invalid function: "weekChanged"' } code=141, message=Invalid function: "weekChanged"`

code:
`Parse.Cloud.define("weekChanged", function(request, response) {
//add record for the user to NotificationCount Class
var currentUserId = request.params.currentUserId;
var notificationLeftWeek;
var userCount = 0;
var NotificationCount = Parse.Object.extend("NotificationCount");
console.log("NotificationCount Query Find Starting");
var query = new Parse.Query(NotificationCount);
query.equalTo("currentUserId", currentUserId);

query.count({
	success: function(count) {
		console.log("NotificationCount Query Count Successful " + count);
		userCount = count;
		
		if (userCount == 0){
			console.log("Inside notificationCount Record Add. Count: " + userCount);
	
			var NotificationCount = Parse.Object.extend("NotificationCount");
			var notificationCount = new NotificationCount();
								
			notificationCount.set("currentUserId", currentUserId);
			notificationCount.set("date", new Date() + "");					
			notificationCount.set("count", notificationAllowedPerWeek);
			notificationCount.set("weekCount",0);
			notificationCount.save(null,{useMasterKey: true});
				success: function(notificationCount){
					response.success(notificationAllowedPerWeek);						   
				},
				error: function(notificationCount, error){
					response.error(error.code + " " + error.message);
				}		   
			});		
		}else{
			var NotificationCount = Parse.Object.extend("NotificationCount");
			console.log("NotificationCount Query Find Starting");
			var query = new Parse.Query(NotificationCount);
			query.equalTo("currentUserId", currentUserId);
			query.find({
				success: function(results){
					for (var i = 0; i < results.length; i++) { 
						var objectNotificationCount = results[i];
						notificationLeftWeek = objectNotificationCount.get('count');
						creationDate = objectNotificationCount.get('date');
							  
						weekCount = objectNotificationCount.get('weekCount');
						
						var date1 = new Date(creationDate);
						var date2 = new Date();
						var one_day=1000*60*60*24;
								
						// Convert both dates to milliseconds
						var date1_ms = date1.getTime();
						var date2_ms = date2.getTime();
						
						// Calculate the difference in milliseconds
						var difference_ms = date2_ms - date1_ms;
							
						// Convert back to days and return
						var diff = parseInt(Math.round(difference_ms/one_day)/7);							
						
						//alert("diff "+diff);
						console.log("Creation time: " + date1);
						console.log("Current time: " + date2);
						console.log("difference min: " + diff);
						console.log("weekCount: " + weekCount);							
						console.log("notificationLeftForWeek: " + notificationLeftWeek);

						if(diff > weekCount && notificationLeftWeek <= notificationAllowedPerWeek){								
							notificationLeftWeek = notificationAllowedPerWeek;
							//	$('#notificationCountServer').html('<strong>'+notificationLeft+'</strong>');
							objectNotificationCount.set("count", notificationLeftWeek);
							objectNotificationCount.set("weekCount",diff);
							objectNotificationCount.save({useMasterKey: true});
							response.success(notificationAllowedPerWeek);									
						}else{
							response.success(notificationLeftWeek);
						}
					}
				},
				error: function(){
					response.error(error.code + " " + error.message);
				}
			});								
		}
	},
	error: function() {			
		//alert("Error: " + error.code + " " + error.message);
		userCount =0;
	}
});

});

Parse.Cloud.define("notification", function(request, response) {
var triggerValue = request.params.triggerValue;
var settingValue1 = request.params.settingKey1;
var settingValue2 = request.params.settingKey2;
var settingValue3 = request.params.settingKey3;
var settingValue3 = request.params.settingKey3;
var currentUserId = request.params.currentUserId;
var notificationLeft;
var objectNotificationCount;

/*Parse.Cloud.run('checkNotificationCount', {currentUserId: currentUserId}, {
	success: function(success) {
		console.log("notificationLeft "+notificationLeft);
		notificationLeft = success;
	},
	error: function(error) {
		alert("Error Find Trigger: " + error + " " + error.message);
	}
});*/

console.log("Send Notification Begin");
var NotificationInitialCount = Parse.Object.extend("NotificationCount");
var queryInitial = new Parse.Query(NotificationInitialCount);
queryInitial.equalTo("currentUserId", currentUserId);
console.log("Send Notification UserId: " + currentUserId);
try{		
	queryInitial.find({
		success: function(results) {
			//alert("Successfully retrieved " + results.length + " scores.");
			// Do something with the returned Parse.Object values
			console.log("success");
			// response.success(results[0].get('count')); 
			for (var i = 0; i < results.length; i++) { 
				objectNotificationCount = results[i];
				notificationLeft = objectNotificationCount.get('count');
			}
			if(notificationLeft > 0){
	  			var query = new Parse.Query(Parse.Installation);
	  			query.equalTo("installationId", request.params.installationId);											
	  			Parse.Push.send({where: query, // Set our Installation query
						data: {					  
							triggerKey:triggerValue, 
							settingKey1:settingValue1,
							settingKey2:settingValue2,
							settingKey3:settingValue3,
							objectType:"android",
							action:"com.huu.library.findmymobile.MESSAGE"
						}
					},{success: function() {
						// Push was successful
						objectNotificationCount.set("count", notificationLeft-1);
						objectNotificationCount.save();
						response.success(notificationLeft-1);								
					},
					error: function(error) {
						// Handle error
						response.error("Error " + error.code + " " + error.message);
					}
				});
			}else{
				response.error("Only " +  notificationAllowedPerWeek + " notifications per week allowed");
			}
		},
		error: function(error) {
			console.log("Error: " + error.code + " " + error.message);
			response.error("Error: " + error.code + " " + error.message);
			alert("Error: " + error.code + " " + error.message);
		}
	});
}catch(e){
	console.log(e);
}
console.log("notificationLeft "+notificationLeft);

});`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants