Supporting Resources:
If you are completing this worksheet it is assumed that you are already familiar with both NodeJS and the Express
package that is used to build dynamic websites. This worksheet will serve as a refresher whilst learning how to use a technique called Test-Driven Development which you will need to apply on a regular basis during the remainder of this module.
If you are not familiar with NodeJS and Express ask your lab tutor for additional resources.
Navigate to the exercises/02_tdd/express/
directory which contains a simple dynamic website that uses the Express web server and the Handlebars templating engine. Examine the folder structure and you will see that there are several directories:
.
├── index.js
├── modules
│ └── todo.js
├── package.json
├── public
│ └── style.css
├── __tests__
│ └── todo.test.js
└── views
├── home.handlebars
└── layouts
└── main.handlebars
- The
index.js
file contains the routes script that controls the website. - The
modules/
directory contains the three modules that will contain the business logic. - the
package.json
file contains the project metadata. - The
public/
directory contains files that can be directly accessed by the web server - The
__tests__/
directory contains test scripts that matche each of the modules - The
views/
directory contains the Handlebars page templates used by the express renderer
and open the package.json
file. Notice that there are certain modules listed as dependencies and others labelled as devDependencies. Use the npm install
command to install all of these.
Start the web server using node index.js
and access the URL (this will be localhost:8080
if you are running it on your workstation). This will display a simple web page containing a form where you can add items to your list. Try adding some items.
One of the most important techniques you can apply to your software is the MVC architectural pattern.
lets see how the todo app implements the MVC Design Pattern.
The model represents the data model used by the app. This includes all business logic and data storage. In the todo app this is represented by the files in the modules/
directory. In our example there is only one module but there could be as many as needed.
The view is the presentation layer of the application. In the todo app this is represented by the contents of the views/
directory. These are the handlebar template files that will be filled with data and sent to the client in the http response.
The controller is responsible for taking the user input (http request), interacting with the model and finally determining what view to send back to the client in the http response. In the todo app this is represented by the index.js
script.
One of the most important tools available to agile development teams is the Unit Testing Framework. This is used to write comprehensive automated tests that cover all the code in the app model. There should be one test suite per module. If you examine the __tests__/
directory you will see that there is a todo.test.js
script that contains the tests for the modules/todo.js
script.
Lets start by running the tests using the npm run test
command. This produces the following output.
add
✓ check there is a single item (4ms)
✓ adding a single item
clear
✓ clear list with items (1ms)
✓ clearing empty list should throw error
getAll
✓ retrieving a single item (1ms)
✓ retrieving empty list should throw an error
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
todo.js | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 0.445s, estimated 1s
As you can see, the tests are grouped by function.
- There are three functions in the
todo.js
script and the tests cover all three. Each test has a human-readable name and the tick✓
marks indicate the tests have passed. - Underneath the list of tests is the _code coverage summary table which indicates how much of the code in your model is covered by the unit tests. If you want more detail, open the
coverage/lcov-report/index.html
file in the web browser which shows how many times each line of the code has been tested. - The final section is a summary that shows the overall status.
Open the todo.test.js
file and see if you can understand how the tests have been written. In the next section you will be writing you own tests so its important you understand the syntax!
Open the manifest file package.json
and study the scripts
object. Can you see what command was triggered when you ran the npm run test
command? What happens when you use the npm run watch
command? Notice that the script doesn't exit! Without interacting with the terminal try editing a file and saving the changes, what happens? How can this help you develop robust code?
Now we can put our newfound skills to the test by learning to program using a technique called Test-Driven Development (TDD). This requires you to follow a short iterative 3-step process:
- First you write a test to define the next bit of functionality to be written. When the test is run it will fail because the functionality has not been written yet (doh!).
- Next you write code to implement the feature, stopping as soon as the new test passes.
- Finally you tidy up (refactor) the code to make it easier to follow making sure the test still passes.
So lets make a start.
Before we can implement any improvements we need to understand how the script currently works, so lets take a look at the form we are submitting, this is in the default layout file and can be found in the views/layouts/main.handlebars
file.
- The form element
<form method="post" action="/">
shows that the form sends a POST request to/
. - Opening the
index.js
file you can see two routes:- The first,
app.get()
handles GET requests. - The second,
app.post()
handles POST requests, this is the route that handles the form data.
- The first,
- The route is simply calling the
todo.add()
function then redirecting back to theapp.get()
route. This means we need to fix thetodo.add()
function. This is in themodules/todo.js
file. - The
add()
function in thetodo.js
file is currently very simple, it creates an object containing the item and quantity data it has been passed then pushes this onto the array.
Try clicking on the Add item button without entering an item name or quantity. What happens? We want to be able to prevent this from happening. Based on our understanding it is clear we need to improve the todo.add()
function.
The first step is to define the functionality we want in the form of a test. Clearly we need to throw an error rather than simply add a blank item to the list. Since we are testing the add()
function we need to add an extra test to this section of the suite.
test('adding a blank string should throw an error', () => {
expect.assertions(1)
try {
todo.add('bread', 1)
todo.add('', '')
} catch(err) {
expect(err.message).toBe('item is blank string')
}
})
Notice that the test expects a single assertion that checks that the error message is correct. Try running the test suite, notice that this new test fails!
add
✓ check there is a single item (6ms)
✓ adding a single item (1ms)
✕ adding a blank string should throw an error (9ms)
clear
✓ clear list with items (1ms)
✓ clearing empty list should throw error (1ms)
getAll
✓ retrieving a single item (1ms)
✓ retrieving empty list should throw an error (1ms)
Step 2 is editing the function so that the test passes. To do this we need to check for a string with a length of 0. Here is the modified function:
module.exports.add = (item, qty) => {
if(item.length === 0) {
throw new Error('item is blank string')
}
data.push({item: item, qty: qty})
}
Try running the test suite again, notice that all the tests now pass. Time to move onto step 3.
The third and final step is to refactor the code, that is to change its structure and layout to improve legibility. In this case, since there is only a single line in the conditional we can remove the curly braces as shown:
module.exports.add = (item, qty) => {
if(item.length === 0) throw new Error('item is blank string')
data.push({item: item, qty: qty})
}
We run the test suite again to make sure we have not broken anything. The tests still all pass so we have now completed our first iteration of TDD!
Now you have completed the first iteration, repeat the TDD process as you solve each of the issues listed below:
- Try entering a non-number in the Qty field, what happens? We need to check that the value is a valid number before adding it to the list. If it is not a valid number, a value of 1 should be used.
- Try leaving the Qty field blank, what happens? In this situation the value of 1 should be used.
- Try adding the same item twice, what happens? Adding the same item should increase the quantity rather than adding a duplicate item.
- Try adding a capitalised item (such as
Bread
) then adding the same item in lowercase (such asbread
), what happens? All list items should be stored in lowercase.
Before we move onto the next task we need to do a manual test of the web page. Restart the server and access the list page.
- Add bread with a quantity of 3
- Add butter with a quantity of 2
- Add bread with a quantity of 2
- Oops! We have a bread quantity of
32
instead of5
. - We have uncovered a bug in our code!
- Oops! We have a bread quantity of
- Can you write a unit test to replicate this bug?
- Hint: you entered the quantity in a textbox.
- The number is being passed as a String.
- Your test should pass two numbers as strings to the function.
- Now modify the
add()
function to pass this new test:- You will need to do some type conversion.
- Once all the tests pass look at the code and refactor it to make it easier to follow.
There is a delete link at the end of every row in the list. What happens when you click on these?
Now you are familiar with writing unit tests and applying a TDD methodology its time to stretch you knowledge by implementing the missing functionality. The route has already been created and there is a delete()
function in the todo.js
script although it currently doesn't do anything.
Apply the TDD methodology to implement the following (this will require 5 iterations):
- Use the Array.prototype.splice() method to delete the array index passed as a parameter to the
delete()
function. - If the index passed in the parameter is larger than the size of the array you should throw an error.
- If the parameter is not a number an error should be thrown.
- If the parameter is not an integer (whole number) an error should be thrown.
- If the parameter is a whole number below
0
an error should be thrown.