Introduction

The FDK comes with a built-in unit testing framework for serverless apps. This framework is built using the popular mochaV5 framework.

Prerequisites

  1. Experience developing apps in Freshworks developer platform
  2. Node js version 10.X
  3. The latest version of FDK with test framework enabled
  4. To check if your FDK version has the test framework enabled, run fdk --version and check if the version is greater than 6.3.x
  5. Basic knowledge in writing unit tests in mocha or other javascript unit testing framework

This tutorial will cover the process of writing test cases for a freshdesk serverless app that creates a webhook in GitHub and saves it in data storage.

The content is intended for the developers with working knowledge of HTML, CSS, JavaScript, and basic knowledge in writing unit tests using mochaJS or other javascript unit testing framework.

The FDK supports local testing serverless apps using simulated events. This is however cumbersome and not easily repeatable as the app grows over time. Automated unit tests are designed to run quickly in a repeatable manner and grow along with the app.

Unit tests also help accelerate review cycles for an app that is to be published and listed on the Freshworks Marketplace. The primary motivation to write and maintain these tests within your app would include,

  1. Code coverage - Cover code paths that otherwise can't be invoked through manual testing
  2. Regression testing - Maintainable, automated tests to help sign off apps before they are submitted for publishing and review

Before starting this section, it's highly recommended to go through the documentation for the test framework to get accustomed to the concept of unit testing.

  1. Clone the marketplace sample apps repository
vel@freshworks ~% git clone git@github.com:freshworks/marketplace-sample-apps.git 
  1. Navigate to the app folder
vel@freshworks ~% cd  marketplace-sample-apps/Play-Along/Exercises/fundamentals/level3
  1. Open the test/server.js file to start writing unit tests

test/server.js

'use strict';

// Assertion library import
const expect = require('chai').expect;

// Payload to invoke events
const appInstallArg = require('./args/onAppInstall.json');
const appUninstallArg = require('./args/onAppUninstall.json');
const externalEventArg = require('./args/onExternalEvent.json');


describe('App test Suite', function() {

// Your test goes here

});

In the following sections, we will write tests to cover all code paths inside all handlers

Using the testing framework, you can write unit tests for a serverless app, similar to any generic unit test written using Mocha and an assertion library. The FDK offers two custom interfaces to help write unit tests

  1. The stub interface: Enables you to provide pre-programmed values that are used to stub specific objects to the app logic. You can stub the following objects (platform features):
  1. The invoke interface: Invokes the serverless event with arguments for which the unit test is written.
  1. Copy the below code and paste under the placeholder in test/server.js
 it('checks onAppInstall Success Flow', function() {

   const stubbedGenerate = this.stub('generateTargetUrl').resolves('http://randomurl.com/webhook');


   const stubbedRequest = this.stub('$request', 'post').resolves({
     response:{
       url: 'http://randomurl.com/webhook'
     }
   });

   const stubbedDB = this.stub('$db', 'set').callsFake(function(key, value) {


     expect(key).to.equal('githubWebhookId');
     expect(value.url).to.equal('http://randomurl.com/webhook');
     stubbedRequest.restore();
     stubbedDB.restore();
     stubbedGenerate.restore();
     return Promise.resolve()
   });
   return this.invoke('onAppInstall', appInstallArg);
 });

 it('checks onAppInstall request Flow', function() {

   const stubbedRequest = this.stub('$request', 'post').rejects({
       error: 'failed to make post request'
   });

   this.invoke('onAppInstall', appInstallArg);
   stubbedRequest.restore();
 });

 it('checks onAppInstalDB fail', function() {

   const stubbedRequest = this.stub('$request', 'post').resolves({
     response:{
       url: 'http://randomurl.com/webhook'
     }
   });
   const stubbedDB = this.stub('$db', 'set').callsFake(function(){

     stubbedRequest.restore();
     stubbedDB.restore();
     return Promise.reject();
   })
   this.invoke('onAppInstall', appInstallArg);
 })


 it('checks if generateTargetURL can be set', function() {
 const stubbedGenerate = this.stub('generateTargetUrl').resolves('http://randomurl.com/webhook');


 const StubbedRequest = this.stub('$request', 'post').callsFake(function(url,payload) {

     expect(payload.json.config.url).to.equal('http://randomurl.com/webhook');
     StubbedRequest.restore();
     stubbedGenerate.restore();
   });

  this.invoke('onAppInstall', appInstallArg)
  });


 it('rejects generateTargetUrl',function(){
   const stubbedGenerate = this.stub('generateTargetUrl').rejects({
     error:'unable to generate target url'
   });

   this.invoke('onAppInstall', appInstallArg);
   stubbedGenerate.restore();

 })

In the code snippet above each, "it(‘comment', function(){ })" block represents a test case, to build a robust application it is suggested to test all possible scenarios including the edge cases.

Let us take "it('checks onAppInstall Success Flow')" for example in which 'generateTargetUrl' is stubbed first to mock the values of output for test purpose, followed by $request and $db

Once the platform features are stubbed, the "onAppInstall" event is invoked with the payload appInstallArg(a JSON file imported at the beginning).

After the "onAppInstall" event is invoked, the handler("onAppInstallHandler" associated with the event inside of src/server.js will be invoked with all the stubbed values and the payload, using which the validations/assertions are done for webhookURL saved in the data storage.

  1. Run the test using fdk test in the project root folder to start the test suite

  1. The above screenshot shows that all the test cases have been passed and shows the coverage report

4. From the test case above we have covered all the code paths and statements in OnAppInstall Handler

  1. Copy the below code and paste it after OnAppInstall
 it('handles app uninstall', function() {

 const stubbedDB = this.stub('$db', 'get').resolves({url:'http://randomurl.com/webhook'});
 const StubbedRequest =  this.stub('$request', 'delete').callsFake(function(data){
   console.log('data',data);
   StubbedRequest.restore();
   stubbedDB.restore();
   return Promise.resolve();

 });

    this.invoke('onAppUninstall',appUninstallArg);

 });

 it('handles App Uninstall events db failure',function(){
   const stubbedDB = this.stub('$db','get').rejects('Db call rejected');
   this.invoke('onAppUninstall',appUninstallArg);
   stubbedDB.restore();
 });

 it('handles App Uninstall events request failure',function(){
   const stubbedDB = this.stub('$db', 'get').resolves({url:'http://randomurl.com/webhook'});
   const StubbedRequest =  this.stub('$request', 'delete').rejects('request call rejected');

   this.invoke('onAppUninstall',appUninstallArg);

   stubbedDB.restore();
   StubbedRequest.restore();

 });
  1. Run the test using fdk test in the project root folder to start the test suite,

  1. From the above screenshot you can see that all the tests have passed, lets got the browser and view the code path covered by our test

  1. Copy the below code and paste it after onAppUninstall section and run fdk test
 it('handles external events',function(){
   const stubbedDB = this.stub('$db','get').resolves({issue_data:{
     ticketID:1
   }});
   const StubbedRequest =  this.stub('$request', 'post').callsFake(function(data){
     console.log('data',data);
     StubbedRequest.restore();
     stubbedDB.restore();
     return Promise.resolve();

   });
   this.invoke('onExternalEvent', externalEventArg)
 })

 it('handles external event db reject', function() {
   const stubbedDB = this.stub('$db','get').rejects({error:'Db call rejected'});
   this.invoke('onExternalEvent', externalEventArg);
   stubbedDB.restore();

 });
  it('handles external event request call reject', function() {
   const stubbedDB = this.stub('$db','get').resolves({issue_data:{
     ticketID:1
   }});
   const StubbedRequest =  this.stub('$request', 'delete').rejects('request call rejected');
   this.invoke('onExternalEvent', externalEventArg);
   stubbedDB.restore();
   StubbedRequest.restore();
 })
  1. Below is the code coverage for onExternalEvent handler

  1. As you can see from the above image we have a decent coverage, but we do not have the recommended 80% coverage for the branches, let's see which part of the code our tests didn't reach in the next section
  1. From the below screenshot, you can see that the lines 124 and 147 are not covered

Assignment :-

Write New test cases or modify the existing use case to cover the lines 124 and 147