testing


How to get started testing with Express, Jasmine, and Zombie

Okay, so I’m a little old fashioned. I’m also lazy and reluctant to learn new things. My buddy Dawson asked How do I get started with testing my web software? I said, start with node/express

Express

Express is a web framework. You build server-side software with Express. Let’s bootstrap a project with express-generator:

1
npx express-generator --view=ejs myapp

This creates a skeleton application from which to launch development. The ejs stands for Embedded Javascript. To install:

1
2
cd myapp
npm install

Execute the server:

1
npm start

Assuming all is well, you can navigate to http://localhost:3000 to see your new app in action. Halt server execution by pressing Ctrl-C.

That’s great and all, but let’s get to testing…

Jasmine

I’m not sure Jasmine is trendy, but I’ve been using it for years. It takes minimal setup and can be neatly structured. It’s a good tool and the tests you write are easily adapted for execution by other test frameworks (if you decide you don’t like jasmine).

Add jasmine to your web application:

1
npm install --save-dev jasmine

jasmine is now a development dependency. Execute cat package.json to get a peak at how node manages its dependencies.

Initialize jasmine like this:

1
npx jasmine init

Most people like to script test execution with npm. Open the package.json file just mentioned. Configure "scripts": { "test": "jasmine" }… that is, make package.json look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "myapp",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"test": "jasmine"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1"
},
"devDependencies": {
"jasmine": "^3.5.0"
}
}

If you do this correctly, you can run the following:

1
npm test

And expect to see something like this:

1
2
3
4
5
6
7
8
9
10
11
12
> myapp@0.0.0 test /home/daniel/workspace/myapp
> jasmine
Randomized with seed 44076
Started
No specs found
Finished in 0.003 seconds
Incomplete: No specs found
Randomized with seed 44076 (jasmine --random=true --seed=44076)
npm ERR! Test failed. See above for more details.

jasmine is telling us that the tests failed because we have yet to write any. Being as lazy as I am, I try to find opportunities to skip unit tests and go directly to testing user behaviour. For this, I use a headless browser called Zombie.

Zombie

Like jasmine, I’m not sure zombie is the trendiest option out there, but I’ve always managed to get the two to play nicely together and have yet to find any serious shortcoming. Add zombie to your project like this:

1
npm install --save-dev zombie

Now we’re ready to write some tests…

Testing!

Oh wait, what’s this app supposed to do? Ummmmm…

For now, I’ll keep it really simple until my buddy Dawson comes up with tougher testing questions.

Purpose

I want to be able to load my app in a browser, enter my name in an input field, press Submit, and receive a friendly greeting in return.

When starting a new web application, the first test I write ensures my page actually loads in the zombie browser. Create a spec file:

1
touch spec/indexSpec.js

I use the word index in the sense that it’s the default first page you land on at any website. Test files end with the *Spec.js suffix by default. Execute cat spec/support/jasmine.json to see how jasmine decides which files to execute.

Open spec/indexSpec.js in your favourite editor and paste this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const Browser = require('zombie');
Browser.localhost('example.com', 3000);
describe('the landing page', () => {
let browser;
/**
* This loads the running web application
* with a new `zombie` browser before each test.
*/
beforeEach(done => {
browser = new Browser();
browser.visit('/', err => {
if (err) return done.fail(err);
done();
});
});
/**
* Your first test!
*
* `zombie` has loaded and rendered the page
* returned by your application. Use `jasmine`
* and `zombie` to ensure it's doing what you
* expect.
*
* In this case, I just want to make sure a
* page title is displayed.
*/
it('displays the page title', () => {
browser.assert.text('h1', 'Friendly Greeting Generator');
});
/**
* Put future tests here...
*/
// ...
});

Simple enough. At this point you might be tempted to go make the test pass. Instead, execute the following to make sure it fails:

1
npm test

Whoa! What happened? You probably see something like this:

1
2
3
4
5
6
7
8
9
10
11
Randomized with seed 73862
Started
F
Failures:
1) the landing page displays the page title
Message:
Failed: connect ECONNREFUSED 127.0.0.1:3000
Stack:
...

It’s good that it failed, because that’s an important step, but if you look closely at the error, connect ECONNREFUSED 127.0.0.1:3000 tells you your app isn’t even running. You’ll need to open another shell or process and execute:

1
npm start

Your app is now running and zombie can now send a request and expect to receive your landing page. In another shell (so that your app can keep running), execute the tests again:

1
npm test

If it fails (as expected), you will see something like this:

1
2
3
4
5
6
7
8
9
10
Randomized with seed 38606
Started
F
Failures:
1) the landing page displays the page title
Message:
AssertionError [ERR_ASSERTION]: 'Express' deepEqual 'The Friendly Greeting Generator'
Stack:
error properties: Object({ generatedMessage: true, code: 'ERR_ASSERTION', actual: 'Express', expected: 'Friendly Greeting Generator', operator: 'deepEqual' })

That’s much better. Now, having ensured the test fails, make the test pass. Open routes/index.js in your project folder and make it look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
// The old command
//res.render('index', { title: 'Express' });
// The new test-friendly command
res.render('index', { title: 'The Friendly Greeting Generator' });
});
module.exports = router;

Execute the tests again:

1
npm test

And you will see:

1
2
3
4
5
6
7
8
9
Randomized with seed 29903
Started
F
Failures:
1) the landing page displays the page title
Message:
AssertionError [ERR_ASSERTION]: 'Express' deepEqual 'The Friendly Greeting Generator'
Stack:

Oh no! Not again! Go back and check… yup, you definitely changed the name of the app. What could be wrong?

You need to restart your server in your other shell. Exit with Ctrl-C and restart with npm start. (Yes, there is a much better way of doing this).

Having restarted your application, execute the tests again with npm test. You will see this:

1
2
3
4
5
6
7
8
Randomized with seed 46658
Started
.
1 spec, 0 failures
Finished in 0.071 seconds
Randomized with seed 46658 (jasmine --random=true --seed=46658

Awesome. Your first test passes. Recall the stated purpose of this app:

I want to be able to load my app in a browser, enter my name in an input field, press Submit, and receive a friendly greeting in return.

Using this user story as a guide, you can proceed writing your tests. So far, the first part of the story has been covered (i.e., I want to be able to load my app in a browser). Now to test the rest…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ...
// Add these below our first test in `indexSpec.js`
it('renders an input form', () => {
browser.assert.element('input[type=text]');
browser.assert.element('input[type=submit]');
});
it('returns a friendly greeting if you enter your name and press Submit', done => {
browser.fill('name', 'Dan');
browser.pressButton('Submit', () => {
browser.assert.text('h3', 'What up, Dan?');
done();
});
});
it('trims excess whitespace from the name submitted', done => {
browser.fill('name', ' Dawson ');
browser.pressButton('Submit', () => {
browser.assert.text('h3', 'What up, Dan?');
done();
});
});
it('gets snarky if you forget to enter your name before pressing Submit', done => {
browser.fill('name', '');
browser.pressButton('Submit', () => {
browser.assert.text('h3', 'Whatevs...');
done();
});
});
it('gets snarky if you forget to enter a blank name before pressing Submit', done => {
browser.fill('name', ' ');
browser.pressButton('Submit', () => {
browser.assert.text('h3', 'Please don\'t waste my time');
done();
});
});
});

You can push this as far as you want. For example, you might want to ensure your audience doesn’t enter a number or special characters for a name. The ones above define the minimal test-coverage requirement in this case.

Make sure these new tests fail by executing npm test. You won’t need to restart the server until you make changes to your app (yes, you should find a better way to manage this).

Make the tests pass

You should try doing this yourself before you skip ahead. I’ll give you a couple of clues and then provide the spoiler. In order to get these tests to pass, you’ll need to add a route to routes/index.js and you’ll need to modify the document structure in views/index.ejs.

Did you try it yourself?

Here’s one possible answer:

Make routes/index.js look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'The Friendly Greeting Generator', message: '' });
});
router.post('/', function(req, res, next) {
let message = 'Whatevs...'
if (req.body.name.length) {
let name = req.body.name.trim();
if (!name.length) {
message = 'Please don\'t waste my time';
}
else {
message = `What up, ${name}?`;
}
}
res.render('index', { title: 'The Friendly Greeting Generator', message: message });
});
module.exports = router;

Make views/index.js look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
<form action="/" method="post">
<label for="name">Name:</label>
<input name="name" type=input" />
<button type="submit">Submit</button>
</form>
<% if (message) { %>
<h3><%= message %></h3>
<% } %>
</body>
</html>

Note the EJS alligator tags (<% %> and <%= %>).

When all expectations are satisfied, you will see something like this:

1
2
3
4
5
6
7
8
Randomized with seed 17093
Started
.....
5 specs, 0 failures
Finished in 0.106 seconds
Randomized with seed 17093 (jasmine --random=true --seed=17093)

What’s next?

Got a test question? What kind of app should I build and test?

If you learned something, support my work through Wycliffe or Patreon.


A non-intrusive behavioural testing approach to bootstrapped React in Typescript

I’m on a team that loves Typescript and React. Their code is manually tested. I prefer to write my tests first.

The following process addresses the problem of respecting an existing approach to app development, while respecting my own standard of professional practice.

This is how I introduced my behavioural testing approach to a project bootstrapped with create-react-app. It equips me to write automated tests while allowing my teamates their own approach.

Generate a React Typescript app

As per the docs, the following produces the base application:

1
2
npx create-react-app my-app --typescript
cd my-app

create-react-app comes with a few baked-in npm scripts. There are two existing build scripts: npm run build and npm start. The first only produces production builds. The latter only produces transient development builds (i.e., you can’t save them for later).

I need to produce test builds, so I add this to this to the scripts section in package.json:

1
2
3
4
5
6
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build:test": "npx env-path -p .env.test react-scripts build"
...
}

create-react-app does a lot of crazy-cool stuff with .env files. I don’t think what follows will respect the dotenv-flow and dotenv-expand features that allows you to cascade build configurations according to environment.

With that it mind, install env-path:

1
npm install --save-dev env-path

I want a test build, so I need a .env.test file. Configurations, of course, are app dependent. The following is one likely example:

1
2
REACT_APP_REDIRECT_URI=http://localhost:3000
REACT_APP_CLIENT_ID=SomeExampleToken123

To create your test build, execute:

1
npm run build:test

You’ll see a message that says:

1
Creating an optimized production build...

Uh oh! react-scripts build only produces a production build. Is it still creating a production build, or is it reading my .env.test file?

This is the perfect opportunity to test if this test build configuration is working…

jasmine and zombie.js

None of this requires testing with jasmine and zombie, but I like testing with this pair because I’m old-fashioned and lazy. Install and initialize:

1
2
npm install --save-dev jasmine zombie
npx jasmine init

NOTE: I’m purposefully skipping adding typescript support to jasmine for the moment. I may revisit this in the future.

Testing the build

I need to determine if the values I set in .env.test are being baked into the test build configured above.

Here’s my first test, which I place in spec/indexSpec.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const path = require('path');
require('dotenv').config({ path: path.resolve(process.cwd(), '.env.test') });
require('../support/server');
const Browser = require('zombie');
const PORT = 3000;
Browser.localhost('example.com', PORT);
describe('landing page', () => {
let browser, document;
beforeEach(done => {
browser = new Browser({ waitDuration: '30s', loadCss: false });
// Wait for React to execute and render
browser.on('loading', (doc) => {
document = doc;
document.addEventListener("DOMContentLoaded", (event) => {
done();
});
});
browser.visit('/', (err) => {
if (err) return done.fail(err);
});
});
it('displays the .env.test config variables', () => {
browser.assert.link('a', 'Redirect', 'http://localhost:3000/&id=SomeExampleToken123');
});
});

If you look closely at the above file, you’ll see this test needs its own server to run (npm start only does a development build). Paste the following into spec/support/server.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express');
const path = require('path');
const app = express();
const logger = require('morgan');
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, '../../build')));
app.get('/*', function(req, res) {
res.sendFile(path.join(__dirname, '../../build', 'index.html'));
});
const port = 3000;
app.listen(port, '0.0.0.0', function() {
console.log(`auth-account listening on port ${port}!`);
});

You need morgan to see the server output:

1
npm install --save-dev morgan

Configure jasmine script

Add another line in the package.json (i.e., the e2e line right below the build:test line already configured):

1
2
3
4
5
6
7
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build:test": "npx env-path -p .env.test react-scripts build",
"e2e": "npm run build:test && npx jasmine"
...
}

The tests should now execute with one failing test:

1
npm run e2e

Make the test pass

The test defined above is simply checking to see if a link is created from the values stored in the .env.test file. Add that link to src/App.tsx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
const App: React.FC = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<!-- Add this link! Right here!!! -->
<a href={`${process.env.REACT_APP_REDIRECT_URI}/&id=${process.env.REACT_APP_CLIENT_ID}`}>Redirect</a>
</div>
);
}

Execute the test:

1
npm run e2e

It passes!

More work…

This configuration doesn’t currently respect the neat dotenv-flow and dotenv-expand features that come baked into create-react-app.

As noted above, jasmine is not currently configured to support typescript.