Express: Creating a Stub Server for Visual Regression Testing
Visual Regression Test Architecture using a Stub Server

Express: Creating a Stub Server for Visual Regression Testing

Continuing my previous post on Automated Visual Regression Testing, I will be discussing here a key aspect of our test architecture that allowed us to have consistent test data - the Stub Server.

Read my previous post here.

In this article, I will show you how easy it is to create your own Stub Server using the awesome express framework. So, grab a coffee (or tea or beer, whatever), sit back and enjoy the ride, I mean the tutorial.

Firstly, provided you have node already installed in your computer, create a project and proceed to install the following:

npm install --save-dev express

npm install --save-dev body-parser

npm install --save-dev cookie-parser

npm install --save-dev nodemon
  • express - fast, unopinionated, minimalist web framework for node.
  • body-parser and cookie-parser - parsers used to easily manipulate cookies and the request body
  • nodemon - is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.

Next, create a folder named src and add the following files:

  1. server.js - this is going to be our entry point and will contain the nodemon configuration
  2. app.js - this file is where we will handle the endpoints requests
  3. route.js - this file is what allows us to route in which folder and what json file is to be served for a particular endpoint request

server.js

const nodemon = require('nodemon');

nodemon({
  script: './src/app.js',
  env: {
    PORT: 4000
  },
  ext: 'js, json',
  watch: ['./src', './public'],
  ignore: ['server.js'],
  delay: 1000,
});

nodemon
  .on('start', () => {
  console.log('Started API Server...');
  })
  .on('restart', (files) => {
    console.log('Watcher has detected files have changed. API Server restarted...');
  });

process.once('SIGINT', () => {
  nodemon.once('exit', () => {
    console.log('nodemon has cleanly exited');
    process.exit();
  });
});
 
  

app.js

const express = require('express');
const http = require('http');
const route = require('./route');
const path = require('path');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const app = express();

app.set('port', process.env.PORT || 4000);
app.use(bodyParser.json());
app.use(cookieParser());

const port = app.get('port');

app.all('/*', (req, res, next) => {
    res.set('Content-Type', 'application/json');
    res.set('Access-Control-Allow-Origin', req.headers.origin);
    res.set('Access-Control-Allow-Headers', 'Content-Type,X-Requested-With,Authorization');
    res.set('Access-Control-Allow-Credentials', 'true');
    res.set('Access-Control-Allow-Methods', 'GET, POST, DELETE, PUT');
    next();
});


// Custom Methods with Cookie Handling
app.get('/customEndpoint', (req, res) => {
    let dir = path.join(__dirname, '/customEndpoint/');
    route.getCustomData(req, res, dir);
});

// Pokemon Methods
app.get('*', (req, res) => {
    let dir = path.join(__dirname, req.path + '/');
    route.getDefaultData(req, res, dir);
});


const defaultResponse = (req, res) => {
    let response = {
        status: "OK",
        message: `${req.method} successful!`
    }
    res.status(200).send(response);
}

app.post('*', defaultResponse);
app.put('*', defaultResponse);
app.delete('*', defaultResponse);


http.createServer(app).listen(port, () => {
    console.log(`API Server listening on port ${port}`);
});
 
  

route.js

const fs = require('fs');
const path = require('path');

let loadJson = (res, dir, file = "success") => {
    let fileName = path.join(dir, file + '.json');
    let json = JSON.parse(fs.readFileSync(fileName, 'utf8'));
    res.status(200).send(json);
};

exports.getCustomData = (req, res, dir) => {
    let file = 'hasNoCookie';
    const token = req.cookies.myCookie;
    if (['hasCookie', 'hasNoCookie'].includes(token)) {
        file = token;
    }
    loadJson(res, dir, file);
};

exports.getDefaultData = (req, res, dir) => {
    loadJson(res, dir);
};

In your package.json, add a script to run the project

"scripts": {
    "start": "node ./src/server.js"
}

Now you can run it using the command:

npm start

The stub server can now be accessed through localhost:4000.

Let's now take a closer look on how to use it. The easiest way to create your test endpoint data is taking advantage of the Pokemon methods:

app.get('*', (req, res) => {
    let dir = path.join(__dirname, req.path + '/');
    route.getDefaultData(req, res, dir);
});

The above code allows us to catch any GET requests that were not handled explicitly. By simply adding a success.json file in the folder path that is the same as the request path, the stub server will serve that file automatically.

For example, say you have a request endpoint localhost:4000/pokemonEndpoint. Under the src folder simply create a folder named pokemonEndpoint and add a success.json file with your data in it:

Now, whenever you hit localhost:4000/pokemonEndpoint, you will receive the data stored in the success.json file.

But what if you have a single endpoint but would like to serve different json data in different situations. Well, no problemo compadre! I have included an example of how to explicitly handle this situation:

# app.js
...
// Custom Methods with Cookie Handling
app.get('/customEndpoint', (req, res) => {
    let dir = path.join(__dirname, '/customEndpoint/');
    route.getCustomData(req, res, dir);
});


# route.js
...
exports.getCustomData = (req, res, dir) => {
    let file = 'hasNoCookie';
    const token = req.cookies.myCookie;
    if (['hasCookie', 'hasNoCookie'].includes(token)) {
        file = token;
    }
    loadJson(res, dir, file);
};

The above code shows you how you can pass cookies and use them to customise the path and file that you want the stub server to serve. In the above example, we have an endpoint at localhost:4000/customEndpoint and we would like to serve a different file depending on the presence of certain cookie value. So, we need to create a folder named customEndpoint and add the files using the cookie names:

Now when hitting the endpoint and passing the specific cookie value, you will be served with the configured json file accordingly. Sweet as!

Testing it in postman:

As an added bonus, since we are using nodemon, we don't need to restart the stub server whenever you add json files or edit the code, nodemon does it for us! Magic!

So there you have it folks, a quick and easy way to create a stub server which you can now use to mock your backend for your visual regression testing. Full source code can be found here.

Stay awesome, Gotham!

marcDacz

Have you considered adding a mode where api calls are proxied and cached / persisted locally for each unique call. Then you would not need to manually add data stubs. Just a suggestion 😊 Cheers!

To view or add a comment, sign in

More articles by Marc Dacanay

  • Mocha + Chai: Common API Endpoint Test Automation

    I recently joined an awesome new company where we'll be undergoing a massive Digital Transformation. The project that I…

    11 Comments
  • BackstopJS: Smart Sync to S3

    This article continues on from my previous posts about our Automated Visual Regression Testing. Here, I'll be…

    2 Comments
  • BackstopJS: A Deep Dive

    In this article, I will try to elaborate where the Visual Regression Test Automation Framework fits in our test…

    3 Comments
  • BackstopJS: My Visual Regression Test Journey

    Recently where I am working, we have been tasked to modularise and modernise our tech stack. The task involves…

    8 Comments

Others also viewed

Explore content categories