AWS Velocity Series: Local development environment

Michael Wittig – 08 Feb 2017

The local development environment is where you make changes to the source code, edit configuration files, add images, and so on. You also want to run the app locally, execute the tests, and be able to debug your source code.

AWS Velocity: Local development environment

In this article you will learn how to setup a Node.js project from scratch with:

  • Unit tests to cover internal functionality
  • Code checks to get rid of typos and code smells early
  • Acceptance tests to verify the end-user behavior

The Node.js app will offer an HTTP API to compute factorials. The concepts can be applied to any other programming language.

AWS Velocity Series

Most of our clients use AWS to reduce time-to-market following an agile approach. But AWS is only one part of the solution. In this article series, I show you how we help our clients to improve velocity: the time from idea to production. Discover all posts!

Let’s get started!

Setup project

You can follow step by step or get the full source code here: https://github.com/widdix/aws-velocity

First, create a folder for the new project. I assume that you use macOS or Linux running a terminal:

mkdir aws-velocity
cd aws-velocity/

Within the new project folder, create folders for your app, infrastructure, deploy scripts, and your acceptance tests.

mkdir app
mkdir infrastructure
mkdir deploy
mkdir acceptance

The project structure should look like this now:

tree ../aws-velocity/
#../aws-velocity/
#├── acceptance
#├── app
#├── deploy
#└── infrastructure
#4 directories, 0 files

Let’s develop the app next.

A simple app

Now you need a simple app that you can use as an example. The app will be based on Node.js but the concepts will apply to all other programming languages. If you don’t have Node.js installed, visit nodejs.org.

cd app/
mkdir test
mkdir lib

Now, create the app structure with the npm init tool. The tool will ask you a bunch of questions, make sure to answer them as I did:

npm init
# name: (app) factorial
# version: (1.0.0) 1.0.0
# description: Factorial as a Service
# entry point: (index.js) index.js
# test command: ./node_modules/.bin/jshint . && ./node_modules/.bin/mocha test/*.js
# git repository:
# keywords:
# author:
# license: (ISC)
# About to write to /Users/michael/Desktop/aws-velocity/app/package.json:
# {
# "name": "factorial",
# "version": "1.0.0",
# "description": "Factorial as a Service",
# "main": "index.js",
# "directories": {
# "test": "test"
# },
# "scripts": {
# "test": "jshint . && ./node_modules/.bin/mocha test/*.js"
# },
# "author": "",
# "license": "ISC"
# }
# Is this ok? (yes) yes

And you need to install a few dependencies. express is a popular web framework, jshint is a code quality tool, and mocha is a test framework:

npm install express@4.14.0 --save
npm install jshint@2.9.4 --save-dev
npm install mocha@3.2.0 --save-dev

jshint needs a little bit of configuration. Create a file .jshintrc with the following content:

app/.jshintrcGitHub
{
"esversion": 5,
"node": true
}

Create a file test/.jshintrc with the following content:

app/test/.jshintrcGitHub
{
"extends": "../.jshintrc",
"mocha": true
}

Create a file .jshintignore with the following content:

app/.jshintignoreGitHub
node_modules/**

Now the app structure is done. Let’s see if the test command works:

npm test
# > factorial@1.0.0 test /Users/michael/Desktop/aws-velocity/app
# > jshint . && ./node_modules/.bin/mocha test/*.js
#
# Warning: Could not find any test files matching pattern: test/*.js
# No test files found
# npm ERR! Test failed. See above for more details.

The tests fail because there are no tests. So let’s add some unit tests for the factorial implementation.

Create a file test/factorial.js with the following content. The structure of the file is determined by the test framework mocha. The important lines start with assert.equal. Those lines contain actual test conditions that the implementation must satisfy:

app/test/factorial.jsGitHub
'use strict';

var factorial = require('../lib/factorial.js');
var assert = require('assert');

describe('factorial', function() {
it('should fail for < 0', function() {
assert.throws(function() {
factorial(-1);
});
});
it('should return 1 for 0', function() {
assert.equal(factorial(0), 1);
});
it('should return 1 for 1', function() {
assert.equal(factorial(1), 1);
});
it('should return 2 for 2', function() {
assert.equal(factorial(2), 2);
});
it('should return 6 for 3', function() {
assert.equal(factorial(3), 6);
});
it('should return 24 for 4', function() {
assert.equal(factorial(4), 24);
});
it('should return 120 for 5', function() {
assert.equal(factorial(5), 120);
});
it('should return 87178291200 for 14', function() {
assert.equal(factorial(14), 87178291200);
});
it('should fail for > 14', function() {
assert.throws(function(){
factorial(15);
});
});
});

If you run npm test again, the test still fail because there is no implementation yet. Let’s change that.

Create a file lib/factorial.js with the following content. The factorial implementation is recursive:

app/lib/factorial.jsGitHub
'use strict';

function factorial(n) {
if (n === 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}

module.exports = factorial;

Let’s see if the tests pass now by running npm test.

Oh no, the implementation is missing some checks for edge cases. Change the file lib/factorial.js accordingly:

app/lib/factorial.jsGitHub
'use strict';

function factorial(n) {
if (n < 0) {
throw new Error('not defined for negative numbers');
}
if (n > 14) {
throw new Error('not implemented for large numbers');
}
if (n === 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}

module.exports = factorial;

Now, wire express up to offer a HTTP endpoint for factorial computation. Create a file index.js with the following content:

app/index.jsGitHub
'use strict';

var factorial = require('./lib/factorial.js');
var express = require('express');
var app = express();

app.get('/:n', function (req, res) {
var n = parseInt(req.params.n, 10);
if (n < 0 || n > 14) {
res.sendStatus(400);
} else {
res.send(factorial(n).toString());
}
});

var port = process.env.PORT || 3000;
app.listen(port, function () {
console.log('app listening on port ' + port);
});

You can now start the app locally:

node index.js
# app listening on port 3000

In another terminal, execute curl to make a HTTP request against your app:

curl http://localhost:3000/5
# 120

So far so good. It’s time to add an acceptance test.

cd ..
cd acceptance/

Now, create the acceptance test structure. The acceptance test is an independent application also using Node.js. The npm init command will setup the project. Make sure to configure it like I did:

npm init
# name: (acceptance) factorial-acceptance
# version: (1.0.0) 1.0.0
# description: Acceptance test for Factorial as a Service
# entry point: (spec.js)
# test command: ./node_modules/.bin/jshint .
# git repository:
# keywords:
# author:
# license: (ISC)
# About to write to /Users/michael/Desktop/aws-velocity/acceptance/package.json:
# {
# "name": "acceptance",
# "version": "1.0.0",
# "description": "",
# "main": "spec.js",
# "scripts": {
# "test": "jshint ."
# },
# "author": "",
# "license": "ISC"
# }
#Is this ok? (yes) yes

And again, you need to install a few dependencies. frisby helps us to test REST APIs, jasmine is yet another test framework, and jshint will ensure code quality:

npm install frisby@0.8.5 --save
npm install jasmine-node@1.14.5 --save
npm install jshint@2.9.4 --save-dev

jshint needs some configuration. Create a file .jshintrc with the following content:

acceptance/.jshintrcGitHub
{
"esversion": 5,
"node": true,
"jasmine": true
}

Create a file .jshintignore with the following content:

acceptance/.jshintignoreGitHub
node_modules/**

Now the app structure is created. Let’s see if the test command works:

npm test

Everything is okay. Now you need to implement the acceptance test.

Create a file factorial_spec.js (the file must end with _spec.js!) with the following content. The structure is determined by frisby, lines with a condition start with .expect:

acceptance/factorial_spec.jsGitHub
'use strict';

var frisby = require('frisby');

if (process.env.ENDPOINT === undefined) {
throw new Error('ENDPOINT environment variable missing');
}

frisby.create('/-1')
.get(process.env.ENDPOINT + '/-1')
.expectStatus(400)
.toss();

frisby.create('/0')
.get(process.env.ENDPOINT + '/0')
.expectStatus(200)
.expectBodyContains('1')
.toss();

frisby.create('/14')
.get(process.env.ENDPOINT + '/14')
.expectStatus(200)
.expectBodyContains('87178291200')
.toss();

frisby.create('/15')
.get(process.env.ENDPOINT + '/15')
.expectStatus(400)
.toss();

To execute the acceptance tests against your locally running app (if you stopped the app, run node index.js in a separate terminal), run:

ENDPOINT="http://localhost:3000" ./node_modules/.bin/jasmine-node .
#Finished in 0.083 seconds
#4 tests, 6 assertions, 0 failures, 0 skipped

The application is done. You have unit tests, and you also have acceptance tests. The Important differences between the two are:

  • Unit tests can run without spawning a web server
  • Unit tests ensure that the factorial function returns the correct values
  • Acceptance tests ensure that the REST API works as expected (e.g. as documented, or as it behaved before)

In the next article, you will learn how to setup the CI/CD pipeline for your new app.

Series

AWS Velocity Cover

  1. Set the assembly line up
  2. Local development environment (you are here)
  3. CI/CD Pipeline as Code
  4. Running your application
  5. EC2 based app
    a. Infrastructure
    b. CI/CD Pipeline
  6. Containerized ECS based app
    a. Infrastructure
    b. CI/CD Pipeline
  7. Serverless app
  8. Summary

You can find the source code on GitHub.

Michael Wittig

Michael Wittig

I’ve been building on AWS since 2012 together with my brother Andreas. We are sharing our insights into all things AWS on cloudonaut and have written the book AWS in Action. Besides that, we’re currently working on bucketAV, HyperEnv for GitHub Actions, and marbot.

Here are the contact options for feedback and questions.