Using RoboHydra as a mock server

By Esteban Manchado Velázquez

Introduction

As you can see from my introductory article Robohydra: a new testing tool for client-server interactions, RoboHydra is a flexible web server that can help you develop and test HTTP-based applications. The first article explored how to use RoboHydra as a proxy for front-end developers. This article will look at a more advanced use case, showing how to employ it as a mock server to build a test suite for your client applications.

When developing a client application, especially one that pulls information from an API, it's useful to have a local web server that will return what we need to test — different features, edge cases, etc. This has several advantages:

  • It's possible to develop an application without access to a staging server (we might be offline, or the server might be inside an intranet that we don't currently have access to).
  • We can generate responses on demand that are often not easy to get from a real server, like race conditions, server down or struggling situations, network latency or slowness, or simply combinations of data that are cumbersome to obtain from the server.
  • We can easily generate responses that are would theoretically never be received from the real server, but need to be allowed for. Examples include malicious server data, corrupted data, data in an obsolete or possible future format, or truncated data.

Brief Recap: A RoboHydra server is composed of many heads. Each head takes care of a URL path, and defines what happens when a request for that path is received. Given a concrete test scenario (e.g. search for a meme template and create an image from it) we can have heads for the different URL paths involved that could return the needed data to reproduce that scenario. So for example, a head listening in /search could return one search result, and a head listening in /make-image could return an image matching the previous search result).

Meme generator example

In this article I will write a mock server to emulate HTTP responses to the Meme panel Opera extension (note that all these techniques work exactly the same for any kind of program that connects to a server via HTTP). Meme panel is an extension that uses the Meme Generator public API to create meme images — it allows you to choose a meme template (say, Sudden Realization Ralph), add some text (say, To test clients / you need a mock server) and obtain the final meme image.

Now I've introduced the example, let's move on to preparing the project.

Preparing the project

Before we start, make sure you have Node installed (at least version 0.6.x; download from http://nodejs.org if necessary). Once that is done, download the code for the Meme panel extension. We need a version that connects to our local mock server instead of the real Meme Generator API server — I've created one, which you can download as a ZIP file. After uncompressing you'll have a directory called MemePanel. This directory is where you will do all the work in this article.

To start with, go to your command line, go into your MemePanel directory, and execute the following command to install RoboHydra:

npm install robohydra

This command will create a directory node_modules. You don't need to do anything with it, just make sure it's inside MemePanel.

Writing our first test head

In the first article we made RoboHydra return content taken from files or other servers; in this case it returns completely "synthetic" data that doesn't exist anywhere else. We'll start with a head that returns a single search result. In the API documentation we can see that the search URL is http://version1.api.memegenerator.net/Generators_Search, and the GET parameter with the search query is called q. As for the response, we can look at the API example, and see the format we have to imitate (reformatted for readability):

{"success":true,
 "result":[
  {"generatorID":45,"displayName":"Insanity Wolf",
   "urlName":"Insanity-Wolf","totalVotesScore":366,
   "imageUrl":"http://cdn.memegenerator.net/images/400x/20.jpg",
   "instancesCount":158030,"ranking":12},
  {"generatorID":488,"displayName":"Insanity Scene Wolf",
   "urlName":"Insanity-Scene-Wolf","totalVotesScore":21,
   "imageUrl":"http://cdn.memegenerator.net/images/400x/74525.jpg",
   "instancesCount":3388,"ranking":197},
  // ...
 ]
}

Armed with this information, we can write a first version of the plugin for the first head: save the following code as MemePanel/robohydra/plugins/memepanelmock/index.js:

var heads               = require('robohydra').heads,
    RoboHydraHeadStatic = heads.RoboHydraHeadStatic;

exports.getBodyParts = function(config) {
    return {
        heads: [
            new RoboHydraHeadStatic({
                path: '/Generators_Search',
                content: {
                    success:true,
                    result: [
                        {generatorID:45,
                         displayName:"Judgmental Developer Bruce Lawson",
                         urlName:"Fake-Bruce-Lawson-Meme",
                         totalVotesScore:9001,
                         imageUrl:"Bruce-Lawson-Template.jpg",
                         instancesCount:9999999,
                         ranking:1}
                    ]
                }
            })
        ]
    };
};

Note that, as we're passing a JavaScript object as the value of the content property, the response content will be the JSON version of that object and the Content-Type header will automatically be set to application/json. See the RoboHydraHeadStatic documentation for full details. Now we're just missing a configuration file to load our new plugin.

Create a file called memepanel.conf inside the MemePanel directory with the following contents:

{"plugins": [{"name": "logger",        "config": {}},
             {"name": "memepanelmock", "config": {}}]}

If you start RoboHydra now with the command node_modules/.bin/robohydra memepanel.conf, you will have a server running our mock search service. It will also write a useful log file in robohydra.log that we can inspect to see the traffic between client and server.

Let's put it to the test by loading the extension in Opera (open or drag the file config.xml from the MemePanel directory into the browser window) and performing a meme search: click on the extension button to open the menu, enter any search terms and click "Search". Note how the result is always the Bruce Lawson meme, no matter what search terms you choose. That's because that head is always returning that content no matter what. You also won't be able to actually create a meme yet, as this is just a simple test condition we have created for our example.

We could teach RoboHydra to only return content when the search terms match a certain criterion, return different content according to the search terms, or even check the incoming search terms for validity, but for now we'll keep it simple. We have total freedom to choose the content of the responses, allowing us to test many situations that might be hard or impossible to obtain with the real server (e.g. to simulate how the extension will behave if the MemeGenerator API changes the format of the responses, or if the server is down, etc). Our test meme doesn't even exist in the real memegenerator.net... at least not yet.

Mocking up the meme image creation

What we have so far allows us to prototype and test the search UI and the result of clicking on a search result, but there's one more step that we need to mock in order to go "full circle" on a basic case: the image creation itself.

The API call for that is Instance_Create, as we can see in the API documentation. We need valid credentials to use that API call so we can't just click on the example link, but we can make our own instance. The response format we have to imitate looks like this:

{success: true,
 result: {
   generatorID: 45,
   displayName: "Judgmental Developer Bruce Lawson",
   urlName: "Fake-Bruce-Lawson-Meme",
   totalVotesScore: 0,
   imageUrl: "http://memegenerator.net/cache/img/400x/0/0/20.jpg",
   instanceID: 22415018,
   text0: "are you saying...",
   text1: "you don't use robohydra?",
   instanceImageUrl: "http://memegenerator.net/instances/400x/22415018.jpg",
   instanceUrl: "http://memegenerator.net/instance/22415018"
 }
}

Now we'll create a new head to handle the /Instance_Create path. Modify MemePanel/robohydra/plugins/memepanelmock/index.js to look like this:

var heads               = require('robohydra').heads,
    RoboHydraHeadStatic = heads.RoboHydraHeadStatic;

exports.getBodyParts = function(config) {
    return {
        heads: [
            new RoboHydraHeadStatic({
                path: '/Generators_Search',
                content: {
                    success:true,
                    result: [
                        {generatorID:45,
                         displayName:"Judgmental Developer Bruce Lawson",
                         urlName:"Fake-Bruce-Lawson-Meme",
                         totalVotesScore:9001,
                         imageUrl:"Bruce-Lawson-Template.jpg",
                         instancesCount:9999999,
                         ranking:1}
                    ]
                }
            }),

            new RoboHydraHeadStatic({
                path: '/Instance_Create',
                content: {
                    success: true,
                    result: {
                        generatorID: 45,
                        displayName: "Judgmental Developer Bruce Lawson",
                        urlName: "Fake-Bruce-Lawson-Meme",
                        totalVotesScore: 0,
                        imageUrl: "Bruce-Lawson-Template.jpg",
                        instanceID: 22415018,
                        text0: "are you saying...",
                        text1: "you don't use robohydra?",
                        instanceImageUrl: "Bruce-Lawson-Final.jpg",
                        instanceUrl: "../robohydra-a-new-testing-tool-for-client-server-interactions/"
                    }
                }
            })
        ]
    };
};

We now have everything in place, so we can restart RoboHydra and check again. Open the extension menu and do the following:

  1. Enter a search term and hit "Search". You should see a single search result, namely the Bruce Lawson meme.
  2. Click on that result. You should see a bigger preview of the meme template together with the text boxes for the top- and bottom-texts.
  3. Write whatever text you like in those boxes and click "Done". You should see the mock result image, which always has the text "Are you saying..." / "you don't use RoboHydra?".

Imitating server failures

So far we have only mocked up results that are relatively easy to obtain with a real server. It's still useful for a variety of reasons, like offline access and deterministic output. But what about checking how the extension behaves when the server returns a 500 Internal Server Error? We could just create a new head returning a 500 status code for all paths, and go to the admin interface to enable/disable certain heads according to what we're testing.

However, when we have more than a handful of such test scenarios, managing all these heads and enabling and disabling the appropriate heads quickly becomes very cumbersome and error-prone. For that reason, RoboHydra allows us to gather all those related heads into tests. You can define as many tests as you want, but only one test is active at a time. When you enable a test, all the heads in that test are automatically enabled, and all heads for any previously active test are automatically disabled.

Let's move the two heads we have created so far to a test called simple, and create a second test called serverDown for the "Server down" scenario. The code for the plugin would end up like this:

var heads               = require('robohydra').heads,
    RoboHydraHeadStatic = heads.RoboHydraHeadStatic;

exports.getBodyParts = function(config) {
    return {
        heads: [
            new RoboHydraHeadStatic({
                name: 'defaultEmptySearch',
                path: '/Generators_Search',
                content: {
                    success:true,
                    result: []
                }
            })
        ],
        tests: {
            simple: {
                heads: [
                    new RoboHydraHeadStatic({
                        path: '/Generators_Search',
                        content: {
                            success:true,
                            result: [
                                {generatorID:45,
                                 displayName:"Judgmental Developer Bruce Lawson",
                                 urlName:"Fake-Bruce-Lawson-Meme",
                                 totalVotesScore:9001,
                                 imageUrl:"Bruce-Lawson-Template.jpg",
                                 instancesCount:9999999,
                                 ranking:1}
                            ]
                        }
                    }),

                    new RoboHydraHeadStatic({
                        path: '/Instance_Create',
                        content: {
                            success: true,
                            result: {
                                generatorID: 45,
                                displayName: "Judgmental Developer Bruce Lawson",
                                urlName: "Fake-Bruce-Lawson-Meme",
                                totalVotesScore: 0,
                                imageUrl: "Bruce-Lawson-Template.jpg",
                                instanceID: 22415018,
                                text0: "are you saying...",
                                text1: "you don't use robohydra?",
                                instanceImageUrl: "Bruce-Lawson-Final.jpg",
                                instanceUrl: "../robohydra-a-new-testing-tool-for-client-server-interactions/"
                            }
                        }
                    })
                ]
            },

            serverDown: {
                heads: [
                    new RoboHydraHeadStatic({
                        path: '/.*',
                        content: "Unhandled exception of some kind (fake)",
                        statusCode: 500
                    })
                ]
            }
        }
    };
};

Go ahead and replace the contents of MemePanel/robohydra/plugins/memepanelmock/index.js with the above code, and restart the RoboHydra server again.

Note that there are no tests enabled by default, so the default head will process requests to /Generators_Search. To see the list of available tests and enable them, go to http://localhost:3000/robohydra-admin/tests and use the controls available there. And remember that you can always see the current heads, including those belonging to tests, at http://localhost:3000/robohydra-admin.

If you enable the serverDown test and try to perform a search using the browser extension, you'll get a message about not being able to connect to MemeGenerator. You can also inspect the file robohydra.log to see all the traffic since you restarted RoboHydra.

Checking the client requests automatically

Up until now, we've been concerned about how RoboHydra is replying to the client — but what about making sure the client sends correct requests? Maybe you're not sure if the client is correctly encoding search terms before sending them to the server. Or maybe you know it's correct now, but want to write a test so you'll know when it breaks. In that case, you'll be delighted to know that RoboHydra heads can contain assertions. The result of those assertions will be saved as part of the current test.

To use assertions, we have to introduce a new kind of head: the RoboHydraHead. This head doesn't have specific behaviour already coded, but instead receives a JavaScript function that will decide what will happen when a request comes. This is the most powerful and flexible RoboHydra head, and in fact all other heads are based on this. As an example, the following code performs the same function as the previous head we created:

new RoboHydraHead({
    path: '/.*',
    handler: function(req, res) {
        res.statusCode = 500;
        res.send("Unhandled exception of some kind (fake)");
    }
})

But you could, of course, specify whatever code you want, and use any Node modules you have installed. For example, you could wait for five seconds before sending a reply, to imitate what happens when there's a proxy error:

new RoboHydraHead({
    path: '/.*',
    handler: function(req, res) {
        setTimeout(function() {
            res.statusCode = 502;
            res.send("Proxy Error (fake, RoboHydra-generated message)");
        }, 5000);
    }
})

In particular, to use assertions you have to accept a second argument in the getBodyParts function, modules. That parameter contains a property assert, which in turn contains several useful assertion functions. Let's create a new test to check the search term encoding.

Modify MemePanel/robohydra/plugins/memepanelmock/index.js to look like this:

var heads               = require('robohydra').heads,
    RoboHydraHead       = heads.RoboHydraHead,
    RoboHydraHeadStatic = heads.RoboHydraHeadStatic;

exports.getBodyParts = function(config, modules) {
    var assert = modules.assert;

    return {
        heads: [
            new RoboHydraHeadStatic({
                name: 'defaultEmptySearch',
                path: '/Generators_Search',
                content: {
                    success:true,
                    result: []
                }
            })
        ],
        tests: {
            simple: {
                heads: [
                    new RoboHydraHeadStatic({
                        path: '/Generators_Search',
                        content: {
                            success:true,
                            result: [
                                {generatorID:45,
                                 displayName:"Judgmental Developer Bruce Lawson",
                                 urlName:"Fake-Bruce-Lawson-Meme",
                                 totalVotesScore:9001,
                                 imageUrl:"Bruce-Lawson-Template.jpg",
                                 instancesCount:9999999,
                                 ranking:1}
                            ]
                        }
                    }),

                    new RoboHydraHeadStatic({
                        path: '/Instance_Create',
                        content: {
                            success: true,
                            result: {
                                generatorID: 45,
                                displayName: "Judgmental Developer Bruce Lawson",
                                urlName: "Fake-Bruce-Lawson-Meme",
                                totalVotesScore: 0,
                                imageUrl: "Bruce-Lawson-Template.jpg",
                                instanceID: 22415018,
                                text0: "are you saying...",
                                text1: "you don't use robohydra?",
                                instanceImageUrl: "Bruce-Lawson-Final.jpg",
                                instanceUrl: "../robohydra-a-new-testing-tool-for-client-server-interactions/"
                            }
                        }
                    })
                ]
            },

            serverDown: {
                heads: [
                    new RoboHydraHeadStatic({
                        path: '/.*',
                        content: "Unhandled exception of some kind (fake)",
                        statusCode: 500
                    })
                ]
            },

            searchTermEncoding: {
                instructions: "Open the extension, search for 'velázquez'",

                heads: [
                    new RoboHydraHead({
                        path: '/.*',
                        handler: function(req, res) {
                            assert.equal(req.queryParams.q,
                                         "velázquez",
                                         "Should encode search terms correctly");
                            res.send(JSON.stringify({
                                success: true,
                                result: []
                            }));
                        }
                    })
                ]
            }
        }
    };
};

Note how the new test has a property we have not seen before, instructions. If this property exists, RoboHydra will show that text when starting the corresponding test. That allows you to give instructions to the user, in case it's a human being executing these tests by hand, or you just want to document what is expected from the client.

Now, restart RoboHydra again, go to the "Tests" section of the admin interface, and start the searchTermEncoding test. Then go to the extension and search for different terms, like "velázquez", "velazquez" and "lawson". After each search, go to http://localhost:3000/robohydra-admin/tests and see the test results under the heading "Current test results". Searching for any terms except "velázquez" will cause the test to fail; searching for "velázquez" will result in a pass, as set out by the assertion contained in this line:

assert.equal(req.queryParams.q, "velázquez", "Should encode search terms correctly");

Conclusion

In this article we have explored another major RoboHydra use case: building mock servers for client testing. We have seen how to make RoboHydra reply with whatever data we need for our tests, and how to make RoboHydra check the client requests. That, together with the first article, gives a good high-level overview of what's possible with RoboHydra. Please comment to let us know what you think, and stay tuned: the next RoboHydra article will explore advanced techniques! Remember to check the official documentation and screen casts too!

Esteban is a quality assurance engineer, project manager, developer, and other things at Opera. He has worked on projects like Opera Link, Opera Unite, My Opera, Dev Opera and others. Outside work, he likes music, playing drums, reading... and hacking, of course.


Comments

No new comments accepted.