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.