Chapter 4. Unit Testing OpenWhisk Applications

A Note Regarding Early Release Chapters

Thank you for investing in the Early Release version of this book! Note that “Unit Testing OpenWhisk Applications” is going to be Chapter 6 in the final book.

For newcomers, probably one of the most challenging parts, when it comes to developing with a Serverless environment, is to learn how to test and debug it properly.

Indeed, you develop by deploying your code continuously in the cloud. So, you generally do not have the luxury to be able to debug executing the code step by step with a debugger.

Note

There is an ongoing project to implement a debugger for OpenWhisk, that is a stripped down local environment.

While a debugger can be in some cases useful, we do not feel that having “the whole thing” locally to run tests is the easiest way to debug your code, nor the more advisable. Instead, developers need to learn how to split their application into small pieces and test it locally before sending the code to the cloud.

OpenWhisk has a complex and large architecture, designed to be run in the Cloud, and developers need to live with the fact that in the long run, they will never have the luxury to have a complete cloud in their laptop.

Developers instead have to learn to test their code locally in small pieces, simulating “just enough” of the cloud environment.

Once they tested the application in small pieces, they can assemble it and deploy as a whole. Then you can run some tests against the final result, generally simulating user interaction, to ensure the pieces are working together correctly.

Note

The name for “testing in small parts” is unit testing, while simulating the interaction against the application assembled is called integration testing. In this chapter, we will focus on how to do Unit Testing of OpenWhisk applications.

Luckily there is plenty of unit testing tools. You can use them to run your code in small parts in your local machine, then deploy only when it is tested. Let’s see how in practice.

Using the Jest Test Runner

OpenWhisk applications generally run in the Cloud. You write your code, you deploy it, and then you can run it. However, to run unit tests, you need to be able to run it in your local machine, without deploying in the cloud.

Luckily it is not so hard to make your code run also locally, but you need to prepare the environment carefully to resemble “enough” the execution environment in the cloud.

We are then going to learn how to run those actions locally, to test them. But before all, we will pick a test tool and learn how to writes tests with it.

Our test tool of choice is Jest. First, we download and install it; then we use it to write some tests. We will also learn how to reduce the amount of test code we have to write using Snapshot Testing.

Using Jest

As we already pointed out, there are a lot of test tools available for Javascript and NodeJS. So many that is difficult to make a choice. But since we need to pick one to write our tests, we selected one that we believe it is a right balance between ease of use and completeness.

We ultimately picked Jest, the test tool developed by Facebook as a complement to testing React applications. We are not going to use React in this book, but Jest is independent of React, and it is a good fit for our purposes.

Some Jest features I cannot live without are:

  • it is straightforward to install

  • it is pretty fast

  • it supports snapshot testing

  • it provides extensive support for “mocking.”

Of course, this does not mean other tools does not have similar features. But we have to settle on someone.

So, assuming you agree with this choice, let’s start installing it. It is not difficult:

$ npm install -g jest                  1
+ jest@22.4.3
updated 1 package in 13.631s
$ jest --version                       2
v22.4.3
1

install Jest as a global command

2

check if jest is now available as a command

Since now we have Jest, we can write a test. Our first test will check a modified version of the word count action (count.js) we used in chapter 2.

The action we are going to test receives its input as a property text of an object args, split the text in words by itself, then it counts words, returning a table with all the words and a count of the occurrences.

Here is the updated version wordcount.js:

function main(args) {
    let words = args.text.split(" ")
    let map = {}
    let n = 0
    for(word of words) {
       n = map[word]
       map[word] = n ? n+1 : 1
    }
    return map
}
module.exports.main = main             1
1

add an export of the function

In OpenWhisk, you can deploy a simple function. It works as long as it as a main function. When you want to use it locally in a test, however, it must be a proper module so a test can import it. So here is an important suggestion:

Tip

Always add the line module.exports.main = main at the end of all the actions. Even if it is not necessary in most cases, it is required when you run a test or when you have to deploy the action in a zip file.

We can now write a test for wordcount that we can run with Jest. A test is composed of 3 important parts:

  • import of the module we want to test with require

  • declare a test with the function test(<description>, <test-body>) where <description> is a string and <test-body> is a function performing the test

  • in the body of the test you perform some assertions in the form expect(<expression>).toBe(<value>) or similar (there are many different toXXX methods; we will see next)

We have to write now a wordcount.test.js with the following code:

const wordcount =
      require("./wordcount").main                1
test('wordcount simple', () => {                 2
    res = wordcount({text: "a b a"})             3
    expect(res["a"]).toBe(2)                     4
    expect(res["b"]).toBe(1)                     5
})
1

import the module wordcount.js under test

2

declare a test

3

invoke the function

4

ensure it found two ``a``s

5

ensure it found one ``b``s

We are ready to run the tests, but before we go on, we need to create a file package.json, as simple as this:

{ "name": "jest-samples" }
Note

If you don’t create a package.json, jest will walk up the directory hierarchy searching for one and will give an error if it cannot find it. Once found, it will assume it is the root directory containing a javascript application and it will look in all the subdirectories searching for files ending in .test.js to search tests in it.

Now you can run the test. You can do it directly using the jest command line from the directory containing all our files:

$ ls
package.json
wordcount.js
wordcount.test.js
$ jest
 PASS  ./wordcount.test.js
  ✓ wordcount simple (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.341s
Ran all test suites matching /wordcount/i.

Jest started to search for tests, found our wordcount.test.js, loaded it and executed all the test in it, then showed the results.

Tip

You usually you do not run jest directly. A best practice is to configure a test command in the package.json, as in the next listing and run it with npm test.

{
  "name": "jest-samples",
  "scripts": {
    "test": "jest"
  }
}

Configuring an Environment to Locally run OpenWhisk actions

So far we have seen how to run tests in general. Now we focus on actually being able to execute tests locally to unit test them.

In general, actions in OpenWhisk runs under NodeJS, so the same code you can execute in your computer with NodeJS should also work in OpenWhisk, as long as the environment is the same.

You need however to match the environment, so you may need to install locally the same libraries and the same version of NodeJS in use in OpenWhisk.

To match the environment, when you run your tests you need to: * run your tests with the same version of Node * have the same Node Packages installed locally * load code in the same way as OpenWhisk load it * provide the same environment variables that are available in OpenWhisk

Let’s discuss those needs in order.

Matching Versions

At the time of this writing, OpenWhisk provides 2 runtimes, one based on NodeJS version 6.14.1 and another based on NodeJS version 8.11.1. In the runtime for 6.14.1 are preinstalled the following packages:

Table 4-1. NodeJS 6 Preinstalled Packages

apn@2.1.2

async@2.1.4

body-parser@1.15.2

btoa@1.1.2

cheerio@0.22.0

cloudant@1.6.2

commander@2.9.0

consul@0.27.0

cookie-parser@1.4.3

cradle@0.7.1

errorhandler@1.5.0

express@4.14.0

express-session@1.14.2

glob@7.1.1

gm@1.23.0

lodash@4.17.2

log4js@0.6.38

iconv-lite@0.4.15

marked@0.3.6

merge@1.2.0

moment@2.17.0

mongodb@2.2.11

mustache@2.3.0

nano@6.2.0

node-uuid@1.4.7

nodemailer@2.6.4

oauth2-server@2.4.1

openwhisk@3.14.0

pkgcloud@1.4.0

process@0.11.9

pug@">=2.0.0-beta6 <2.0.1”

redis@2.6.3

request@2.79.0

request-promise@4.1.1

rimraf@2.5.4

semver@5.3.0

sendgrid@4.7.1

serve-favicon@2.3.2

socket.io@1.6.0

socket.io-client@1.6.0

superagent@3.0.0

swagger-tools@0.10.1

tmp@0.0.31

twilio@2.11.1

underscore@1.8.3

uuid@3.0.0

validator@6.1.0

watson-developer-cloud@2.29.0

when@3.7.7

winston@2.3.0

ws@1.1.1

xml2js@0.4.17

xmlhttprequest@1.8.0

yauzl@2.7.0

Note

In the environment with the NodeJS 8.11.1 are installed only openwhisk@3.15.0, body-parser@1.18.2 and express@4.16.2.

The best way to prepare the same environment for local testing is to install the same version of NodeJS using the tool nvm , then install locally the same packages that are available in OpenWhisk.

For example, let’s assume you have an application to be run under the NodeJS 6 runtime using the library cheerio for processing HTML. You can recreate the same environment locally using the Node Version Manager NVM as described in https://github.com/creationix/nvm.

Actual installation instructions differ according to your operating system, so we do not provide them here.

Once you have NVM installed, the right version of NodeJS installer, you can install the right environment for testing with the following commands (I redacted the output for simplicity):

$ nvm install v6.14.1                            1
Downloading https://nodejs.org/dist/\
  v6.14.1/node-v6.14.1-darwin-x64.tar.xz...
######### 100,0%
Now using node v6.14.1 (npm v3.10.10)
$ node -v
v6.14.1
$ npm init --yes                                 2
Wrote to  chapter4/package.json
$ npm install --save cheerio@0.22.0              3
chapter4@1.0.0 chapter4
└─┬ cheerio@0.22.0
1

install the version of NodeJS used in OpenWhisk

2

create a pacakge.json to store package configuration

3

install locally the cheerio library v0.22.0

Best Practices for Unit Testing Actions

An action sent to OpenWhisk is a module, and you can load it with require. In general, OpenWhisk allows to create an action without having to export it if it is a single file, but in this case, you cannot test it locally because require wants that you export the module. Furthermore, if you’re going to test also functions internal to the module, you have to export them, too.

So, for example, let’s reconsider the module for validation of an email in Chapter 3, Strategy Pattern, and let’s rewrite the file email.js it in a form that you can test locally. Also, we make the error messages parametric (we will use this feature in another test later).

const Validator = require("./lib/validator.js")
function checkEmail(input) {                           1
  var re = /\S+@\S+\.\S+/;
  return re.test(input)
}
var errmsg = " does not look like an email"
class EmailValidator extends Validator {
    validator(value) {
        let error = super.validator(value);
        if (error) return error;
        if(checkEmail(value))                          2
            return "";
        return value+errmsg
    }
}
function main(args) {
    if(args.errmsg) {                                  3
        errmsg = args.errmsg
        delete args.errmsg
    }
    return new EmailValidator("email").validate(args)
}
module.exports = {
  main: main,                                          4
  checkEmail: checkEmail                               5
}
1

validation logic for the email isolated in a function

2

here we use the function to validate email

3

parametric error message

4

the main function must be exported always

5

also we want to test the checkEmail

Let’s write the email.test.js, step by step. At the beginning we have to import the two functions we want to test from the module:

const main = require("./email").main
const checkEmail = require("./email").checkEmail

We can then write a test for the checkEmail function:

test("checkEmail", () => {
 expect(
  checkEmail("michele@sciabara.com"))
    .toBe(true)
 expect(
  checkEmail("http://michele.sciabarra.com"))
    .toBe(false)
})

Now we add another test, testing instead the main function, the one we wanted to test in the first place:

test("validate email", () => {
 expect(main({email: "michele@sciabarra.com"})
   .message[0])
   .toBe('email: michele@sciabarra.com')
 expect(main({email:"michele.sciabarra.com"})
   .errors[0])
   .toBe('michele.sciabarra.com does not look like an email')
})

Setting OpenWhisk Enviroment Variables

In OpenWhisk you use the OpenWhisk API to interact with other actions. We already discussed in Chapter 3 how you invoke actions, fire triggers, read activations and so on.

When you want to invoke other actions running in the same namespace as the main action, you need to use require("openwhisk") and then you can access the API. At least, this is what happens your application is running inside OpenWhisk.

Note

It is worth to remind that when an action runs inside OpenWhisk, it has access to environment variables containing authentication information that the API use as credentials to execute the invocation.

When your code is running outside OpenWhisk, as it happens when you are running unit tests it, the OpenWhisk API cannot talk with other actions because every request needs authentication. Hence, if you try to run your code outside of OpenWhisk, you will get an error.

We can demonstrate this fact considering a simple action dbread.js, able to read a single record we put in the database with the Contact Form:

const openwhisk = require("openwhisk")
function main(args) {
  let ow = openwhisk()
  return ow.actions.invoke({
    name: "patterndb/read",
    result: true,
    blocking: true,
    params: {
      docid: "michele@sciabarra.com"
    }
  })
}
module.exports.main = main

If we deploy and run the action in OpenWhisk there are no problems:

$ wsk action update dbread dbread.js
ok: updated action dbread
$ wsk action invoke dbread -r
{
    "_id": "michele@sciabarra.com",
    "_rev": "5-011e075095301fcfc350cac66fd17c7e",
    "type": "contact",
    "value": {
        "name": "Michele",
        "phone": "1234567890"
    }
}

However, let’s try to write and run a test dbread.test.js for this simple action:

const main = require("./dbread").main
test("read record", () => {
   main().then(r => expect(r.value.name).toBe("Michele"))
})

If we run the test, the story is a bit different:

$ jest dbread
 FAIL  ./dbread.test.js
  ✕ read record (8ms)

  ● read record

    Invalid constructor options. Missing api_key parameter.

      1 | const openwhisk = require("openwhisk")
      2 | function main(args) {
    > 3 |   let ow = openwhisk()
      4 |   return ow.actions.invoke({
      5 |     name: "patterndb/read",
      6 |     result: true,

      at Client.parse_options (node_modules/openwhisk/lib/client.js:80:13)
      at new Client (node_modules/openwhisk/lib/client.js:60:25)
      at OpenWhisk (node_modules/openwhisk/lib/main.js:15:18)
      at main (dbread.js:3:12)
      at Object.<anonymous>.test (dbread.test.js:5:4)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.021s
Ran all test suites matching /dbread/i.

As we discussed in Chapter 3 when we introduced the API, the OpenWhisk library needs to know the host to contact to perform requests (the API host) and must provide an authentication key to be able to interact with it.

When your application is running in the cloud in OpenWhisk, those keys are available through two environment variables \\__OW_API_HOST and \\__OW_API_KEY. It is the OpenWhisk environment that sets them before executing the code.

When your code is running locally, those variables are not available, unless your test code takes care of setting them. Luckily, it is it pretty easy to provide them locally. Indeed, if you are using the tool wsk and you have configured properly, it stores credentials in a file named .wskprops in the home directory.

Because the format of this is compatible with the syntax of the shell, if you are using bash (as we always assumed you are doing in this book) you can load those environment variables with the following commands:

$ source ~/.wskprops
$ export __OW_API_HOST=$APIHOST
$ export __OW_API_KEY=$APIKEY

Now, with those environment variables set, you can try again to run the test, and this time you will be successful:

$ jest dbread
 PASS  ./dbread.test.js
  ✓ read record (22ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.951s, estimated 1s
Ran all test suites matching /dbread/i.

Your local code is using credentials and can contact and interact with the real server to run the test.

Note

This test is not a unit test. It does not run entirely on your local machine. Contacting a real OpenWhisk is a good idea for running integration tests. However, when you write unit tests, you should provide a local implementation that simulates the real servers. We will discuss in detail how to simulate OpenWhisk in the paragraph in this chapter when we introduce mocking.

Tip

For simplicity, if you want your enviroment to be properly set when you run the test, we recommend probably to leverage the npm test command to run tests and initialize them properly. You can add the entry in the following listing into your package.json. This way you will be able to run tests with just npm test without having to worry to setup manually the environment variables.

 "scripts": {
    "test": "source $HOME/.wskprops;               1
 __OW_API_HOST=$APIHOST
 __OW_API_KEY=$AUTH jest"
  },
1

this is actually one long line split in three lines for typesetting purposes

Snapshot Testing

A common problem, in the test phase, is the burden of having to check results. You may end up to writing a lot of code that the result against the expected values. Luckily, there are techniques (that we are going to see) to reduce the amount of repetitive and pointless code you have to write.

When you test, you decide on a set of data corresponding to the various scenarios you want to verify. Then you write your test, invoking your code against the test data, and check the results. While defining the test data is generally an exciting experience, because you have to think about your code and what it does, verifying that the results are the expected one is typically not an exciting activity.

Jest provides a large number of “matchers” to make easy result verification. Matchers work well when results are small. But sometimes test results are pretty large; hence the code to check the result can be not only dull but also substantial.

There is an alternative to automate this activity, to focus more on checking the results and less on writing repetitive and naive code: snapshot testing.

The idea of snapshot testings is pretty simple: when you run a test the first time, you save the test results in a file. This file is called a “snapshot.” You can then check the result is correct. You do not have to write code, only verify the expected result checking the snapshot.

If the result is correct, you can commit the snapshot into the version control system. When you rerun a test, if there is already a snapshot in place, the test tool will automatically compare the new result of the test with the snapshot. If it is the same, the test passes, otherwise, it fails.

Let’s see snapshot testing in Jest with an example. You implement snapshot testing using the simple matcher called toMatchSnapshot().

We now modify the two tests in the preceding paragraph to use snapshot testing. While the test for checkEmail simply checks if the result is true or false (so there is not any significant advantage in using snapshot testing), the test for validateEmail is a bit more complicated.

Indeed, we wrote a test expressions like this:

expect(main({email: "michele@sciabarra.com"})
   .message[0])
   .toBe('email: michele@sciabarra.com')
expect(main({email: "michele.sciabarra.com"})
   .errors[0])
   .toBe('michele.sciabarra.com does not look like an email')

Here we are taking the test output, extracting some pieces (.message[0] or .errors[0]) to verify only a part of the result.

We could save yourself from the burden of thinking which parts to inspect just writing:

test("validate email with snapshot", () => {
  expect(main({email: "michele@sciabarra.com"})
   .toMatchSnapshot()
  expect(main({email: "michele.sciabarra.com"})
   .toMatchSnapshot()
})

Now, if you run the test, Jest will save the results of the tests. Of course, we should always check that the snapshot is correct. Here is the first execution of the tests, when you create the snapshots:

$ jest email-snap
 PASS  ./email-snap.test.js
  ✓ validate email with snapshot (7ms)

 › 2 snapshots written.                         1
Snapshot Summary
 › 2 snapshots written in 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   2 added, 2 total                   2
Time:        0.899s, estimated 1s
1

running the test creates two snapshots

2

summary of the snapshots in the test

Jest creates a folder named __snapshots__ to store snapshots; then, for each test, it writes a file named after the filename containing the snapshots:

__snapshots__/email-snap.test.js.snap

This file is human readable, and it was designed e expecting that a human can reads it to be sure the result of the snapshot are as expected.

For example, this is the content of the snapshot file just created:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`validate email with snapshot 1`] = `  1
Object {
  "email": "michele@sciabarra.com",
  "errors": Array [],
  "message": Array [                           2
    "email: michele@sciabarra.com",
  ],
}
`;

exports[`validate email with snapshot 2`] = `  3
Object {
  "email": "michele.sciabarra.com",
  "errors": Array [                            4
    "michele.sciabarra.com does not look like an email",
  ],
  "message": Array [],
}
`;
1

result of the first test, with a correct email

2

we produce an array of messages

3

result of the second test, with a wrong email

4

we produce an array of errors

Now, once we have verified the snapshot to be correct, all we have to do is to keep it, saving in the version control system. If the snapshot is already present, running again the tests will compare the test execution against the snapshot, as follows:

$ jest email-snap.test.js
 PASS  ./email-snap.test.js
  ✓ validate email with snapshot (7ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   2 passed, 2 total
Time:        0.876s, estimated 1s

Updating a Snapshot when something change

Once a snapshot is in place, Jest will verify the tests always returns the same result. But of course, snapshots cannot be set on stone. It may happen that something change and we will have to update the snapshot. So let’s make now an example of a failed test verified comparing a snapshot, then update that snapshot.

For example, assume we decide that from now we want to consider acceptable only emails having a dot in the username. This example is, of course, non-realistic, made only to add another constraints to fail on purpose some snapshot tests and act as a consequence.

So, we change the regular epression to validate the emails:

before: var re = /\S+@\S+\.\S+/;
after : var re = /\S+\.\S+@\S+\.\S+/;

If we now run the tests, we see they are now failing:

FAIL ./email-snap.test.js
  ✕ validate email with snapshot (17ms)

  ● validate email with snapshot

    expect(value).toMatchSnapshot()

    Received value does not match stored snapshot 1.

    - Snapshot
    + Received

      Object {
        "email": "michele@sciabarra.com",
    -   "errors": Array [],
    -   "message": Array [
    -     "email: michele@sciabarra.com",
    +   "errors": Array [
    +     "michele@sciabarra.com does not look like an email",
        ],
    +   "message": Array [],
      }

      2 |
      3 | test("validate email with snapshot", () => {
    > 4 |     expect(main({email: "michele@sciabarra.com"})).toMatchSnapshot()
      5 |     expect(main({email: "michele.sciabarra.com"})).toMatchSnapshot()
      6 | })
      7 |

      at Object.<anonymous>.test (testing/strategy/email-snap.test.js:4:52)

 › 1 snapshot test failed.
Snapshot Summary
 › 1 snapshot test failed in 1 test suite. Inspect your code changes or re-run \
      jest with `-u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 passed, 2 total
Time:        0.967s, estimated 1s
Ran all test suites matching /email-snap.test.js/i.

We can fix the test replacing the first email:

expect(main({email: "michele.sciabarra@gmail.com"}))
.toMatchSnapshot()

However, if we run the test again, it will still be failing because the snapshot expects a michele@sciabarra.com somewhere.

We need to update the tests using jest -u. The flag -u tells to Jest to discard the current content of the snapshot cache and recreate them.

After updating tests of course, you should check that the new snapshot is correct. Indeed we have now:

 "message": Array [
    "email: michele.sciabarra@gmail.com",
  ],

and the new snapshot is ready to be used for further tests.

How to Implement Mocking

For a surprisingly large part of many applications, you can run tests in isolation. However, at some point, while testing your code, you will have to interface with some external entities.

For example, in OpenWhisk development, two commons occurrences are the interaction with other services in http, or invoking the OpenWhisk API, for example, to invoke other actions or activate triggers. It is here where the boundaries of unit testing and integration test starts to blur.

An inexperienced developer would say that is impossible to locally test code that uses external APIs: you need to deploy your code in the real environment actually to see what happens.

This idea is not real. Indeed it is possible to simulate “just enough” of the behavior of an external environment (or of any other system) implementing a local “work-alike” of the remote system. We call this a mock.

In a mock, you generally do not have to provide all the complexities of the remote service: you only deliver some results in response to specific requests.

Simulating external environments by Mocking them is another fundamental testing technique. In our case, Jest explicitly supports mocking providing features to replace libraries with mocks we define.

We will see first how mocking generically works, starting with a simple example simulating an http call.

Afterward, we are going to see in detail how to mock a significant part of the OpenWhisk API, to be able to simulate complex interaction between actions.

Mocking an https request

The concept behind mocking is pretty simple: you run your code replacing a system you interface with, using a simulation that you can execute locally. The simulation generally runs without connecting to any real system and providing dummy data.

To better understand how it works, and which problem mocking solves, let’s consider a common problem: testing code that executes a remote HTTP call. We are going for this purpose to write a simple action httptime.js, that just return the current time.

However, to make it a case of an action “hard” to test it, the implementation is going to use another action, invoked via http, that will return the date and time in full format, from where our action will extract the time.

We will have to use the NodeJS https module, that is a bit low level; also we need to wrap the call in a Promise, to conform to OpenWhisk requirements. For those reasons the code is a bit more convoluted than we would like it to be. However, since mocking https call is a frequent and important case, it is worth to follow this example in full.

The action to be tested by Mocking

The code in the following listing, implementing an action that returns current time via another action providing date and time, works this way:

  • wraps everything in a promise, providing a resolve function

  • opens an http request to an url, provide as a parameter

  • define event handlers for two events: data and end

  • collect data as they are received

  • at the end extract the time with a regular expression

const https = require("https")

function main(args) {
  return new Promise(resolve => {                    1
    let time = ""                                    2
    https.get(args.url, (resp) => {                  3
      resp.on('data', (data) => {                    4
        time += data                                 5
      })
      resp.on('end', () => {                         6
        var a = /(\d\d:\d\d:\d\d)/.exec(time)        7
        resolve({body:a[0]})                         8
      })
     })
   })
}
1

wrap everything in a promise

2

this variable collects the input

3

use the https module

4

handle the data event, new data received

5

collect the data

6

handle the end event, all data received

7

this regexp extract the time from the date

8

return the value extracted

For convenience, let’s test first the real thing, deploying it in OpenWhisk. We need to create also the action service that returns the current time and date.

Note the following commands, that create on the fly an action on the command line and pass its url to our action:

$ CODE="function main() {\                           1
>  return { body: new Date() } }"
$ wsk action update testing/now <(echo $CODE) \      2
   --kind nodejs:6 --web true
$ URL=$(wsk action get testing/now --url | tail -1)  3
$ curl $URL                                          4
2018-05-02T19:42:34.289Z
1

store some code in a variable

2

use a bash feature to create a temporary file

3

get the url of the newly created action

4

invoke the action in http

We can now deploy and invoke the action to see if it works:

$ wsk action update testing/httptime httptime.js \
  -p url "$URL" --web true
$ TIMEURL=$(wsk action get testing/httptime --url | tail -1)
$ curl $TIMEURL
20:06:55

Mocking the https module

The example action we just wrote is a typical example of an action difficult to unit test, for many common reasons:

  • it invokes a remote service, so to run tests you need to be connected to the internet

  • you also need to be sure the invoked service is readily available

  • data returned changes every time, so you cannot compare results with fixed values

Hence we have a is a perfect candidate for testing by mocking the remote service. We need to replace the https call with a mock that will not perform any remote call. Let see how to do it with Jest.

To replace the https module with a mock the following steps are required:

  • put code that replaces the https modules with your code in a directory called __mocks__

  • use jest.mock('https') in your test code before importing the module to test

  • write your test code as if you were using the real service

Note

The location of the __mocks__ folder must be in the same of your packages.json, sibling to the node_modules folder.

Tip

The jest.mock call is required only to replace built-in NodeJS modules. In general, modules in the __mocks__ directory will be used automatically as a mock before importing any module from node_modules and will have the priority.

When you perform a require in a jest test, it will load your mocking code instead of the regular code. Since we want to replace the https module, we need to write a __mocks__\https.js mock code.

Now it is time to write the mock code that should replace the https call.

To better understand it, let’s split the description into two steps. At the top level, it works mostly in this way:

https.get(URL, (resp) =>
   resp.on('data', (data) => {

     // COLLECT_DATA

   }

   resp.on('end', () => {

      // COMPLETE_REQUEST

   }

}

The “real” https module receives an object (resp) as a gatherer of event handling functions, that mimic how https works. The protocol https, when performing a request, opens a connection to an URL, then read and return results in multiple chunks. For this reason, you can have multiple calls to the data event executing the code marked as COLLECT_DATA. Hence you need to collect data before analyzing them. When all the data are collected, you get one (and only one) invocation to execute the code marked as COMPLETE_REQUEST.

If we want to emulate this code with a mock, we need to do the following steps:

  1. first create an object that store the event handler

  2. invoke once (or more) the data event handler

  3. invoke once the end event handler

So, the code to gather the events is:

var observer = {               1
   on(event, fun) {            2
       observer[event] = fun   3
    }
}
1

create an object

2

define an on function

3

assign to a field named as the event a function

This code is simple, but is it not so obvious. It defines an object that can register event handlers, and then can invoke them by name, as follows:

observer.on('something', doSomething)

observer.something(1)

So if you pass the observer to the https.get function, you will get being able to invoke the two handlers registered. Code will register resp.on('data', dataFunc) and resp.on('end', endFunc). Afterwards you will invoke just resp.data("some data") and resp.end().

Now that we have our observer, the full mock is much simpler to write.

// observer here, omitted for brevity

function get(url, handler) {       1

    handler(observer)              2

    observer.data(url)             3

    observer.end()                 4

}

module.exports.get = get
1

it will be invoked as https.get(url, handler)

2

to fully understand this, review the first listing in this section

3

important: we are using the URL itself as the value returned by the mocked https call

4

invoke the end handler

Note

Pay attention to the trick of using the URL as the data returned itself. In short, our mock for an URL https://something will return something! Since the only parameter we are passing to our mock is the url, whose meaning is in our case irrelevant because we are not executing any remote call, it makes sense to use the URL itself as a result.

Now, we can use the our __mock__/https.js to write a test https.test.js:

jest.mock('https')                        1
const main = require('./httptime').main   2
test('https', () => {                     3
  main({
    url: '2000-01-01T00:00:00.000Z'       4
  }).then(res => {                        5
    expect(res.body).toBe('00:00:00')     6
  })
})
1

enable the mock

2

import the action locally for test

3

declare a test

4

invoke the main, remember we use the URL as the data!

5

the action returns a promise, so we have to handle it

6

check the handler extracts the time from the date

To be sure, we try to rerun the test with different data:

test('https2', () => {
  main({ url: '2018-05-02T19:42:34.289Z' })
  .then(res => {
    expect(res.body).toBe('19:42:34')
  })
})

The final result:

$ jest httptime
 PASS  ./httptime.test.js
  ✓ https (5ms)
  ✓ https2 (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.222s
Ran all test suites matching /httptime/i.

Mocking the OpenWhisk API

Now that we learned about testing with mocking, we can use it to test OpenWhisk actions using the OpenWhisk API without deploying them. In this paragraph, we are going to see precisely this: how to perform unit testing of actions that are invoking other actions without deploying them.

The OpenWhisk API was covered in Chapter 4. We developed a mocking library that can mock the more frequently used features of OpenWhisk: action invocation and sequences.

The library is not very large, but explaining all the details can take a lot of space, and it is not useful in advancing our knowledge of OpenWhisk. It is mostly an exercise in JavaScript and NodeJS programming.

So we are not going to describe in details its implementation, we only explain how to use it.

The library itself, together with the examples of this book is available on GitHub at this address:

Repo:
http://github.com/openwhisk-in-action/chapter7-debug
Path:
__mocks__/openwhisk.js

To use it in your tests, you need to install Jest then download and place the file openwhisk.js in your __mocks__ folder.

You can then use this mocking library to write and run local unit tests that include action invocations and action sequences.

Using the mocking library to invoke an action

To use our library, we have to follow some conventions in the layout of our code.

When your application runs in OpenWhisk, you do not have the problem of locating the code when invoking an action. You deploy them with the name you choose, then use this name in the action invocation. It is the OpenWhisk runtime that resolves the names.

When you test your code locally and simulate invocations by mocking, you do not deploy your action code, but you still invoke it by name. So our mocking library must be to locate the actual code locally. It does following some conventions.

To illustrate the conventions it follows, let’s see an example. We write a test that will invoke an action using the OpenWhisk API.

const ow = require('openwhisk')             1
test("invoke email validation", () => {
    ow()                                    2
      .actions.invoke({                     3
        name: "testing/strategy-email",     4
        params: {
          email: "michele.sciabarra.com"    5
        }
      }).then(res =>
        expect(res).toMatchSnapshot())
})
1

initialization of the OpenWhisk library

2

create the actual invocation instance

3

perform the invocation

4

action name

5

parameters

You must translate the testing/strategy-email name in a file name located in the local filesystem. We use, as the base in the filesystem, the folder with our package.json and node_modules, that is the project root.

By convention, we name our actions according to this structure:

<package>/<prefix>-<action>

Then we expect actions code to be placed in a file named:

<package>/<prefix>/<file>.js

So, when you test locally code invoking the testing/strategy-email action, the mocking library works in this way:

  • the require("openwhisk") will actually load the __mocks__/openwhisk.js library

  • the mocking library can locate its position from the __dirname variable, hence find the project root (that is, the parent directory)

  • using the name of the action, it can now locate the code of the action to invoke: in our case <project-root>/testing/strategy/email.js

Mocking action parameters

There is another essential feature required to complete our simulation for testing.

In OpenWhisk, actions can have additional parameters. Those parameters can be specified when you deploy the action or can be inherited from the package to which the action belongs. When we use our library mocking library, we can simulate those parameters by adding a file with the same name as the action and extension .json.

For example, in our case we have the file testing/strategy/email.json, in the same folder as the email.js with the content:

{
    "errmsg": " is not an email address"
}

The mocking library uses this file, preloading the parameters and passing them as args. Indeed we can check the test has used the parameter errmsg. We need to check the snapshot to see:

$ cat __snapshots__/invoke.test.js.snap                    1
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`invoke email validation 1`] = `
Object {
  "email": "michele.sciabarra.com",
  "errors": Array [
    "michele.sciabarra.com is not an email address",      2
  ],
  "message": Array [],
}
`;
1

dump on the terminal the snapshot file

2

the error message produced is the one specified in the email.json file

Mocking a sequence

We can now complete our analysis of the mocking library by describing how to mock a sequence.

In Chapter 5 we saw the pattern “Chain of Responsibility” implemented as a sequence.

A sequence does not have a corresponding action, but it does exist as a deployment declaration only. We can simulate an action sequence with a particular value in the JSON file we use to pass parameters.

For example, let consider a test to a sequence:

test('validate4', () =>
 ow().actions.invoke({
   name: 'testing/chainresp-validate',
   params: {
     name: 'Michele',
     email: 'michele.sciabarra.com',
     phone: '1234567890'
   }
}).then(res => expect(res).toMatchSnapshot()))

There is not a testing/chainresp/validate.js file, but there is a testing/chainresp/validate.json with this content:

{
  "__sequence__": [
    "testing/strategy-name",
    "testing/strategy-email",
    "testing/strategy-phone"
  ]
}

You can now write a test for example as follows:

const ow = require('openwhisk')
test('validate', () =>
  ow().actions
    .invoke({
      name: 'testing/chainresp-validate',
      params: {
        name: 'Michele',
        email: 'michele.sciabarra.com',
        phone: '1234567890'
      }
    })
    .then(res => expect(res).toMatchSnapshot()))

The mocking library then read the __sequence__ property from the validate.json and translate in a sequence of invocations (as it would happen in the real OpenWhisk) allowing to test the result of a chained invocation locally.

We can see that the mocking of the sequence works inspecting the snapshot.

// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validate 1`] = `
Object {
  "email": "michele.sciabarra.com",
  "errors": Array [
    "michele.sciabarra.com is not an email address",  1
  ],
  "message": Array [
    "name: Michele",
    "phone: 1234567890",
  ],
  "name": "Michele",
  "phone": "1234567890",
}
`;
1

error message defined as a parameter

Summary

In this chapter we have seen:

  • How to use Jest for Testing OpenWhisk Action

  • How to configure a local OpenWhisk work-alike environment for NodeJS

  • How to use Snapshot Testing

  • How to mocking a generic library

  • How to use an OpenWhisk mocking library