Skip to content

Instantly share code, notes, and snippets.

@tochman
Last active August 28, 2021 17:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tochman/87e5fda80c56e01c7237ef0ec7888361 to your computer and use it in GitHub Desktop.
Save tochman/87e5fda80c56e01c7237ef0ec7888361 to your computer and use it in GitHub Desktop.

Setting up the project structure

Technology

  • Server: superstatic
  • Implementation code location: src
  • Test locations:
    • Acceptance tests: cypress/integration
    • Unit tests: specs
  • Test dependencies:
    • Acceptance tests: Cypress
    • Unit tests: Mocha, Chai, Sinon, bdd-lazy-var
  • Helper deps: start-server-and-test

Objective

  • set up the folder structure
  • add dependencies for running and testing the app
  • configure the dependencies to run the application and tests

Step-by-step

$ 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 to false 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 the specs forlder as the main folder for all out tests. We will group them in subfolders.

Create the app structure

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 the bdd-lazy-var library
  • --recursive rund specs in sub-folders of specs if there are any
  • --exit: shuts down Mocha efter the last test case has been executed
  • specs/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 running yarn start from CLI)
  • http://localhost:3000: tell the package to wait for http://localhost:3000 to fully load
  • cy:open: that executes your "cy:open" scripte (equivaletnt of running yarn cy:open from CLI)

The first Cypress test

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 be describe()) helps you to organixe your tests in relevant scenarios pr topics.
  • it(): (can also be specify()) is the test case that will assert that your code behaves as intended. You can execute code in the it block.

There are several other components of a test case that we will be working with later (hooks like: before(), beforeEach(), after() and afterEach()).

More config of unit test flow

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment