Skip to content

Server Side Testing

JT edited this page Nov 26, 2016 · 6 revisions

Quick Overview of Server Side Testing

Our Implementation of Server Side Testing

Technologies used

Why we chose them

  • Jasmine is essencial for testing client-side Angular2 and we believe in continuity; no need to use a different framework for the server-side when we're using one already

Server-side testing consists of a spec and integration tests for the router and controller of the endpoint

  • Router:
    • Primary assumptions to test for:
      • is the router being called.
      • was the function that was called the correct one.
  • Controller:
    • Primary assumptions to test for:
      • does the controller function output match expectations

Examples

router.ts

/**
 * GET     /api/wonder            ->  index
 * POST    /api/wonder            ->  create
 * GET     /api/wonder/:id        ->  show
 * PUT     /api/wonder/:id        ->  update
 * DELETE  /api/wonder/:id        ->  destroy
 */

let express = require('express');
import * as controller from './wonder.controller';

let router = express.Router();

router.get('/', controller.index);
router.get('/:id', controller.show);
router.post('/', controller.create);
router.put('/:id', controller.update);
router.patch('/:id', controller.update);
router.delete('/:id', controller.destroy);

export {router as wonderRoutes};

spec.ts

import proxyquire = require('proxyquire');
let pq = proxyquire.noPreserveCache();
import sinon = require('sinon');

let wonderCtrlStub = {
  index: 'wonderCtrl.index',
  show: 'wonderCtrl.show',
  create: 'wonderCtrl.create',
  update: 'wonderCtrl.update',
  destroy: 'wonderCtrl.destroy'
};

let wonderRouterStub = {
  get: sinon.spy(),
  put: sinon.spy(),
  patch: sinon.spy(),
  post: sinon.spy(),
  delete: sinon.spy()
};

// require the index with our stubbed out modules
let wonderIndex = pq('./wonder.router.js', {
  'express': {
    Router: function() {
      return wonderRouterStub;
    }
  },
  './wonder.controller': wonderCtrlStub
});

describe('Wonder API Router:', function() {

  it('should return an express router instance', function() {
    expect(wonderIndex.wonderRoutes).toEqual(wonderRouterStub);
  });

  describe('GET /api/wonders', function() {

    it('should route to wonder.controller.index', function() {
      expect(wonderRouterStub.get.withArgs('/', 'wonderCtrl.index').calledOnce)
        .toBe(true);
    });

  });

  describe('GET /api/wonders/:id', function() {

    it('should route to wonder.controller.show', function() {
      expect(wonderRouterStub.get.withArgs('/:id', 'wonderCtrl.show').calledOnce)
        .toBe(true);
    });

  });

  describe('POST /api/wonders', function() {

    it('should route to wonder.controller.create', function() {
      expect(wonderRouterStub.post.withArgs('/', 'wonderCtrl.create').calledOnce)
        .toBe(true);
    });

  });

  describe('PUT /api/wonders/:id', function() {

    it('should route to wonder.controller.update', function() {
      expect(wonderRouterStub.put.withArgs('/:id', 'wonderCtrl.update').calledOnce)
        .toBe(true);
    });

  });

  describe('PATCH /api/wonders/:id', function() {

    it('should route to wonder.controller.update', function() {
      expect(wonderRouterStub.patch.withArgs('/:id', 'wonderCtrl.update').calledOnce)
        .toBe(true);
    });

  });

  describe('DELETE /api/wonders/:id', function() {

    it('should route to wonder.controller.destroy', function() {
      expect(wonderRouterStub.delete.withArgs('/:id', 'wonderCtrl.destroy').calledOnce)
        .toBe(true);
    });

  });

});

controller.ts

import * as _ from 'lodash';
import Wonder from './wonder.model';

// if the wonder object was not found
function handleEntityNotFound(res) {
  return function(entity) {
    if (!entity) {
      res.status(404).end();
      return null;
    }
    return entity;
  };
}

// if there was an error of any kind return approapriate status code
function handleError(res, statusCode = null) {
  statusCode = statusCode || 500;
  return function(err) {
    res.status(statusCode).send(err);
  };
}

function rndInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

// When we update a wonder we are actually replacing an existing wonder
function updateWonder(res, wonder) {
  return function(entity) {
    if (entity) {
      // wonder = old wonder
      entity.name = wonder.name;
      entity.created = new Date().toISOString();
      // find new indeger for the x and y coors
      entity.xcoor = rndInt(5, 80);
      entity.ycoor = rndInt(10, 70);
      entity.save((err, wonder) => {
        // if there's an error than send a 400 code with error message
        if (err)
          res.status(400).json(err.errors.name);
        // send back 200 code with new wonder json
        res.json(wonder);
      });

    }
    return null;
  };
}

// IF the response needs the wonder abject as well
function respondWithResult(res, statusCode = null) {
  statusCode = statusCode || 200;
  return function(entity) {
    if (entity) {
      res.status(statusCode).json(entity);
      return null;
    }
  };
}

// save new wonder endpoint: not replace
function saveUpdates(updates) {
  return function(entity) {
    let updated = _.merge(entity, updates);
    return updated.save()
      .then(update => {
        return update;
      });
  };
}

// remove wonder endpoint
function removeEntity(res) {
  return function(entity) {
    if (entity) {
      return entity.remove()
        .then(() => {
          res.status(204).end();
        });
    }
  };
}

// Gets a list of Wonders
export function index(req, res) {
  return Wonder.find().exec()
    .then(respondWithResult(res))
    .catch(handleError(res));
}

// Gets a single Wonder from the DB
export function show(req, res) {
  return Wonder.findById(req.params.id).exec()
    .then(handleEntityNotFound(res))
    .then(respondWithResult(res))
    .catch(handleError(res));
}

// Creates a new Wonder in the DB
export function create(req, res) {
  return Wonder.findOne({}).sort({ created: 1 }).exec()
    .then(updateWonder(res, req.body))
    .catch(handleError(res));
}

// Updates an existing Wonder in the DB
export function update(req, res) {
  if (req.body._id) {
    delete req.body._id;
  }
  return Wonder.findById(req.params.id).exec()
    .then(handleEntityNotFound(res))
    .then(saveUpdates(req.body))
    .then(respondWithResult(res))
    .catch(handleError(res));
}

// Deletes a Wonder from the DB
export function destroy(req, res) {
  return Wonder.findById(req.params.id).exec()
    .then(handleEntityNotFound(res))
    .then(removeEntity(res))
    .catch(handleError(res));
}

integration.ts

import app from '../../server';
import request = require('supertest');

let addr = app.get('address');

// Wonder endpoint Testing
describe('Wonder API:', function() {
  let newWonder;
  let wonders;

  // constant wonder array for testing purposes
  const inputs = [1, 43, 2, 35, 65, 36, 10, 57, 32, 45, 90, 79, 32];

  // Testing the POST api endpoint
  describe('POST /api/wonders', function() {
    // For the sake of conserving space, test using for loops
    for (let counter = 0; counter < inputs.length; counter++) {
      (function (input) {
        // set up beforeAll's for every possible wonder that will be inputed
        // all will execute and POST necessary wonders to test
        return beforeAll((done) => {
          request(addr)
            .post('/api/wonders')
            .send({
              name: 'wonder: ' + input
            })
            .expect(200)
            .expect('Content-Type', /json/)
            .end((err, res) => {
              if (err) {
                done.fail(err);
              }
              expect(res.body.name).toBe('wonder: ' + input);
              done();
            });
        });
      })(inputs[counter]);
    }

    // Simple true = true to start the wonder POSTs
    it('should respond back each query with inputted wonder', () => {
      expect(true).toBe(true);
    });
  });

  // This is where the real testing begins
  describe('GET /api/wonders', function() {
    // beforeAll GET all wonders from the DB
    beforeAll(function(done) {
      request(addr)
        .get('/api/wonders')
        .expect(200)
        .expect('Content-Type', /json/)
        .end((err, res) => {
          if (err) {
            done.fail(err);
          }
          wonders = res.body;
          done();
        });
    });

    // This response should be an array
    it('should respond with JSON array', function() {
      expect(wonders).toEqual(jasmine.any(Array));
    });

    // Loop through the response array and expect the output to be the same as
    // the POSTs above
    it('wonders should equal the original input array', () => {
      for (let i = 0; i < 10; i++) {
        (function (input, counter) {
          return expect(wonders[counter].name).toBe('wonder: ' + input);
        })(inputs[i + 3], (i + 3) % 10);
      }
    });
  });
});