- Server:
superstatic
- Implementation code location:
src
- Test locations:
- Acceptance tests:
cypress/integration
- Unit tests:
specs
- Acceptance tests:
- Test dependencies:
- Acceptance tests: Cypress
- Unit tests: Mocha, Chai, Sinon, bdd-lazy-var
- Helper deps: start-server-and-test
- set up the folder structure
- add dependencies for running and testing the app
- configure the dependencies to run the application and tests
$ yarn init -y
Create the folder structure for out implementation code and our tests:
$ mkdir src
$ mkdir specs
$ mkdir specs/features
$ mkdir specs/units
Add superstatic
as a dependency:
$ yarn add superstatic
Next, install the unit tests dependencies:
$ yarn add -D mocha chai sinon sinon-chai bdd-lazy-var start-server-and-test
Create a specHelper.js
in the specs
folder:
$ touch specs/specHelper.js
Add Cypress as a development dependency:
$ yarn add -D cypress
Finish the installation of Cypress:
$ ./node_modules/.bin/cypress open
// or
$ npx cypress open
Then click on the "No thanks, delete example files" link. Then stop Cypress by terminating the window.
Cypress created a file in the root folder of the project that is named cypress.json
. Please add the following configuration to that file:
{
"baseUrl": "http://localhost:3000",
"chromeWebSecurity": false,
"integrationFolder": "./specs/features"
}
Let me explain what is going on:
"baseUrl"
: Saves you time by not having to constantly repeat the address of your app in tests"chromeWebSecurity"
: Setting this tofalse
alows you to access any domain (local or not) without experiencing cross-origin errors"integrationFolder"
: We will overwrite the deafult setting of Cypress as to where the test file will be located. It this project, we want to use thespecs
forlder as the main folder for all out tests. We will group them in subfolders.
Create a index.html
in the src
folder and add a basic html 5 document sructure to it.
Add a <h1>
with the content "Address Book" to the <body>
tag:
<body>
<h1>Address Book</h1>
</body>
$ touch src/index.html
Create a js
subfolder with 2 files: app.js
and AddressBook.js
$ touch src/js/app.js
$ touch src/js/AddressBook.js
All in all, you should have the following structure of folders and files:
.
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── plugins
│ │ └── index.js
│ └── support
│ ├── commands.js
│ └── index.js
├── cypress.json
├── package.json
├── specs
│ ├── features
│ │ └── userCanVisitTheApplication.feature.js
│ ├── specHelper.js
│ └── units
├── src
│ ├── index.html
│ └── js
│ ├── AddressBook.js
│ └── app.js
└── yarn.lock
Let's focus on the necessary scripts we'll need to be able to run the app and our tests. First out will be the script to start the app. In package.json
add the "scripts"
key (set the value to an object {}
). Add the "start"
scritp to it:
"scripts": {
"start": "superstatic src --port 3000"
}
Next, we want to configure the script for running Mocha. Add the following script to the "scripts"
key:
"test": "mocha --file specs/specHelper.js -u bdd-lazy-var/global --recursive --exit specs/units"
Let me explain what is going on:
mocha
: runs the test framework--file specs/specHelper.js
: pre-loads our helper methods-u bdd-lazy-var/global
tells Mocha to use thebdd-lazy-var
library--recursive
rund specs in sub-folders ofspecs
if there are any--exit
: shuts down Mocha efter the last test case has been executedspecs/units
: point Mocha to the folder where your unit tests are located
If you execute this script, you should see a message similar to this:
$ mocha --file specs/specHelper.js -u bdd-lazy-var/global --recursive --exit specs
0 passing (1ms)
✨ Done in 0.25s.
Moving on to Cypress commands. Add these two scripts that will make use of the start-server-and-test
library:
"cy:open": "cypress open",
"cypress": "start-server-and-test start http://localhost:3000 cy:open"
Let me explain what is going on:
start-server-and-test
: use that package to run execute what follows...start
: that executes your"start"
scripte (equivaletnt of runningyarn start
from CLI)http://localhost:3000
: tell the package to wait forhttp://localhost:3000
to fully loadcy:open
: that executes your"cy:open"
scripte (equivaletnt of runningyarn cy:open
from CLI)
In the specs/features
folder, we will now create a new test file. Name this file as follows:
$ touch specs/features/userCanVisitTheApplication.feature.js
Add the following describe
block and the test case within it (the it
block):
context("User visits the application url", () => {
it("is expected to display a header", () => {
cy.visit("/");
cy.get("h1").should("contain.text", "Address Book");
});
});
So what is going on here?
context()
: (can also bedescribe()
) helps you to organixe your tests in relevant scenarios pr topics.it()
: (can also bespecify()
) is the test case that will assert that your code behaves as intended. You can execute code in theit
block.
There are several other components of a test case that we will be working with later (hooks like: before()
, beforeEach()
, after()
and afterEach()
).
Let's turn our focus to unit testing and the helper methods we want to add to our specHelper.js
.
We want to make certain Chai and Sinon functionality available in our tests. That is why we tell Mocha to preload the specHelper
file.
Let's add the following code to specHelper.js
:
const { use, expect } = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
use(sinonChai);
global.expect = expect;
global.sinon = sinon;
We want the sinon
and expect
to be globally available, and for that reason we add them as properties of the global
object.
Next, we want to create a very basic test to see if our configuration works.
Create a test file in the specs/units
folder and call it AddressBook.spec.js
$ touch specs/units/AddressBook.spec.js
Add the following code to the test file:
const AddressBook = require("../../src/js/AddressBook");
describe("AddressBook", () => {
subject(() => new AddressBook());
it("is expected to be an Object", () => {
expect($subject).to.be.an("object");
});
it("is expected to be an instance of AddressBook class", () => {
expect($subject).to.be.an.instanceof(AddressBook);
});
});
And execute the test from your terminal:
$ yarn test
You should see a lot of error messages (I just show the top part here:)
yarn run v1.22.10
$ mocha --file specs/specHelper.js -u bdd-lazy-var/global --recursive --exit specs/units
AddressBook
1) is expected to be an Object
2) is expected to be an instance of AddressBook class
0 passing (4ms)
2 failing
1) AddressBook
is expected to be an Object:
TypeError: AddressBook is not a constructor
//more....
We need write a proper implementation of the AddressBook
class in the src/js/AdressBook.js
:
class AddressBook {}
module.exports = AddressBook;
At this stage the test should pass for you. We will bind some functionality to that object as we move along.
As a side note, even though it is a bit out of scope of this particular section, we can do a bit of refactoring of our tests. Comment out or remove the it
-blocks we added in the example above, and put those in place instead:
it(() => is.expected.to.be.an("object"));
it(() => is.expected.to.be.an.instanceof(AddressBook));
These one-liners do exactly the same as the ones we added initially, but save us the effort of writing test messages. Neat, huh?
Another important detail before we move on. In order to test the AddressBook
class, we need to be able to import it into our test file, right? We do that in AdressBook.spec.js
near the top:
const AddressBook = require("../../src/js/AddressBook");
The import is made possible because we export
the module from the implementation file (src/assets/js/AdressBook.js
).
module.exports = AddressBook;
Now, the problem is that when we will use this class in the context of the actual application, we will not be able to do that. There is simply no support for module
in the browser environment. This piece of code will lead to an error and your application will not work. I know that this might be premature, and we should probably deal with this issue when it actually arise. But let's make sure that now, so it does not hit us later when we leave the test evironment and run our app live in the brawser - where it is intended to live after all.
What we need to do is to make sure that we can run this code no matter if it is being used in the browser, or in node environment. This conditional will achieve that for us:
if (typeof module !== "undefined" && module.exports) {
module.exports = AddressBook;
}
At this stage we should be good to go with implementing some logic to our AddressBook
class. Well, there is a bit of configuration/programming we need to deal with before we can start coding on the AddressBook
, and that is to make sure that our test environment has access to a storage solution we will use. For that, we will have to take a step back and do some research.