- Lab
- Core Tech

Guided: Testing a Node.js and Express Application
This lab introduces learners to the methodology of Test-Driven Development(TDD) along with Unit and Integration Testing. Learners are given a partially complete Express CRUD application wherein they will be guided on writing and running tests along with implementing the features to pass those tests. Learners should be familiar with JavaScript and Express to fully grasp the context of this lab.

Path Info
Table of Contents
-
Challenge
Introduction
Overview
In this lab, you will be introduced to the methodology of test-driven development(TDD) and how it can be employed through two of the most common testing methods: Unit testing and Integration testing. To do this, you will be utilizing the
Mocha
test runner framework along with theChai
assertion library. Some tests will elso employSinon
to mock certain behavior and other will useSupertest
.
Application
You have been provided with a nearly complete CRUD (Create, Read, Update, Delete) Express application to manage some generic products. For demonstration, a few initial routes, route handlers, and their respective tests have already been implemented. The rest have not, and you will be guided on how to do so.
Further Details
The data for this application is persisted through a JSON file called
main-database
located in the/data
directory. Thetest-database
JSON file will only be used for testing, which will be covered in a later step. If at anytime you wish to "reset" the data inmain-database
after you make changes, you can run thenpm run seedItems
command in the Terminal.
Tips
If you'd like to check your work, the
solutions
directory contains files for you to compare with. However, these solutions aren't the only valid solutions, so don't feel pressured to make your implementations exactly the same. What's important is that you understand what the tests are meant to do rather than writing them "correctly".Now let's continue on to TDD in the next step.
-
Challenge
Test-Driven Development
Overview
Test-Driven Development, or TDD, is a development methodology where tests are written before you actually implement any features, functions, or logic. Typically how this works is that you design a test case, or a scenario, for what a feature should or shouldn't do. It is important to always have a balance between robust tests that handle as many edge cases or possible flaws, but also isn't too rigid to the point where you must meet certain semantics to pass the test.
After the test has been written, you will write the code needed to make sure the test passes. Then it becomes an iterative process where you will continue to refactor/improve your code(and perhaps even the test) while still continuing to pass the test(s). There will be an example of this for you to see in this lab, but for brevity's sake it won't be a recurring objective for the tests and functions you implement.
Tests in this lab will include two of the most common types of tests, which are Unit Tests and Integration Tests. The next step will address Unit Testing.
-
Challenge
Unit Testing
Overview
Unit Testing is one of many testing techniques and is usually the most commonplace. It involves writing tests targeting individual components or functions in an application. For this lab, you will be working in the
tests/UnitTests.test.js
andcontrollers/dbController.js
files for unit tests.
Setting Things Up
Open up the
dbController
file and take a look. The methodsreadData()
andwriteData()
are already done as they relate to retrieving and storing data into the respective JSON file. The methodgetProductById()
is already defined as it will be the example for you to look at. The rest of the methods will require you to implement. Now openUnitTests.test.js
.Don't get too overwhelmed by everything here, as it will be explained to you piece by piece. The top has the usual imports from the
chai
assertion library and stubs for mocking from thesinon
library. Thedescribe
functions are unique to themocha
testing framework and are used to define testing suite for a group of tests. In other words, think of it like a directory where each test case is a file in that directory. Theit
functions are the actual tests being run. ThebeforeEach()
/afterEach()
hooks and data stub variables aren't something you need to worry about. They are there to setup/mock data before each test case and to clean up afterwards. You need to mock data in the scenario because in unit testing, you don't want to modify the actual data in the database yet, just test that the functions themselves work as intended.
The First Unit Test
Now look at the first test case, identified as
'testGetID'
. ThegetProductById()
method defined indbController
usesreadData()
, but you don't need to read the data from the database, especially if you could end up accidentally modifying it. This is why the tests use thereadDataStub
, which will substitute thereadData()
call to returnproducts
data instead. Next, the test passes in 2 validid
values togetProductById()
as well as a nonexistent one. If implemented properly,getProductById()
should return the correct product when given valid id's and anundefined
value if no product with the matching id is found. Lastly, it checks that thereadDataStub
has been called 3 times since the method usesreadData()
once each time it's called. Since it's called 3 times,readData()
should have been called thrice. You can run this test in the terminal with the commandnpm test -- -g 'testGetID'
and it should pass. -
Challenge
Unit Testing Continued
Next, let's look at the second test case, which is identified as
testAdd
. Like the previous test, it starts by defining some mock data and stubbing it in. It also defines some example products to be added. Then it callsaddProduct
and passes in one of the example products before checking that the outcome is as expected; the array should increase in size and the new product in the array should be what was passed in. Since the implementation ofaddProduct()
should also utilizereadData()
andwriteData()
, it should also check that they are called and thatwriteData()
is being called withproducts
. The next section simply repeats this process with the second example product, but you will notice that it checks that the stubs are being called twice now instead once since you are doing the process again.
Adding Products
This unit test is complete and you can run it with the command
npm test -- -g 'testAdd'
, but it should fail becauseaddProduct()
has not been fully implemented. Go to it indbController.js
and finish the method implementation.Instructions (1)
* The `newId` has already been defined since this application will use an incrementing `id` value as a unique identifier. * The `product` parameter this method receives is an object that has a `name` and `quantity` property. * Use `push()` to add a new object to `products` such as `{id: newId, name: product.name, quantity: product.quantity }`. * Call `writeData()` and in `products`. Remember that like `readData()`, `writeData()` is asynchronous and is an instance method. * Run the test again and take notes of any feedback if the test fails, otherwise it should pass.
Updating Products
Head over to the
testUpdate
test case and you'll notice the initial data has already been created for you. You'll need to finish a unit test forupdateProduct()
. It borrows a lot fromtestAdd
so use that test case as a reference.Instructions (2)
* Call `updateProduct()` and pass in one of the updated products. * Check that the length of the `products` array is still the same(length of 5), and that the product at the corresponding index equals the corresponding `updatedProduct`. For instance, `updatedProduct1` corresponds to index 1. * Just like in the `addProduct()` test case, you should also check that the `readDataStub` and `writeDataStub` were called once and that `products` was passed into `writeDataStub`.
At this point, you can optionally choose to do the same process again for the other updated product, or create another of your own and test it against the mock data. Remember from earlier that robust tests are always a balance between coverage and flexibility; if you don't cover enough edge cases then it would be too unreliable, but cover too much and the test becomes brittle and inflexible. At the very least, it would definitely be a good idea to do the test with the
nonExistentProduct
, as nothing should happen to the data when given theid
of a nonexistent product.Instructions (3)
* Call `updateProduct()` on the `nonExistentProduct`, then check that the `products` array length has not been changed. * Since `updateProduct()` always calls `readData()`, then `readDataStub()` should have been called accordingly. However, `writeData()` should not be called since there is no matching `id`, so it should only be called as many times as you tested before. * Now execute this test and it should fail because you still need to implement `updateProduct()`.
Find the method
updateProduct()
indbController.js
. You will see thatupdateProduct()
takes anupdatedProduct
parameter, which is an object that has andid
property. Implement this method.Instructions (4)
* Find the index of the product in `products` whose `id` matches the `id` of `updatedProduct` using the `findIndex` method as all the products have a unique `id`. * If a match is found, set the product at that index to `updatedProduct` and call `writeData()` with `products`. If no matching product is found, then result of `findIndex` will be -1 and you should not do anything. * Optionally, you could print out a console message that states this occurred. Now run the test again and it should pass. * If it does not, take note of the feedback from the test or take a look in the `solutions` directory.
Deleting Products
Lastly, head to the
testDelete
test case inUnitTests.test.js
. You'll need to unit test and implementdeleteProduct()
. Much of it will be similar to the previous two unit tests you've implemented.Instructions (5)
* Pick an item from `products` to delete and pass its `id` to `deleteProduct()`. * Then use `splice()` on `products` to delete the product from the local `products` array. For instance, passing in `3` as the `id` to `deleteProduct()` would require you to `splice()` from index 2. * Verify that the stubs have been called just like in previous tests, including the check that `writeDataStub` was called with `products`. * You can do this multiple times if you wish, but remember that checking your stub calls will increment for each repetition. You can also test for the same behavior with a nonexistent id as you did in `testUpdate`. * As before, run the test and it should fail.
Now head back to
deleteProduct()
indbController.js
and implement it.Instructions (6)
* This method takes an `id` parameter, which you can use with the `filter` method to create a new array consisting of all products whose `id` does not match this parameter. * If this new array does not match the original `products` array, update the JSON database with `writeData()` just like you did in `updateProduct()`. * Do not modify anything otherwise, though you can once again optionally print a message to the console if that is the case. * Now run your unit test once more and it should pass if `deleteProduct()` is implemented correctly. -
Challenge
Integration Testing
Overview
With unit testing finished, you will now want to take a look at some integration testing. Integration testing is usually the next step in the hierarchy of testing. Whereas unit testing involves testing individual components or functions, integration testing involves testing the interactions between different components in the system. In the context of this application, integration testing would involve testing the route endpoints and the route handlers that are executed when the endpoint is called. As such, you will now need to work with the
/tests/IntegrationTests.test.js
,/controllers/ProductController.js
, and/routes/products.js
files.
Routes
Take a look at
products.js
. This is where all the route endpoints are defined, and calling them will trigger the associated route handler function. As you can see, there are already 2 routes defined for handling GET and POST requests to the same/create
endpoint, which will execute their respective handler functions. You will need to define the routes and implement the handler functions for updating and deleting products.
Route Handlers
Next, head over to
productController.js
, where you will see 5 functions suffixed by whether they are GET or POST requests. These methods are the route handlers. The functionproduct_create_get()
just renders thecreate-update-form
and passes in atitle
, whileproduct_create_post()
takes the provided data from the form and to add a new product to the database, before redirecting back to the home page.
Finally, let's look at the integration tests in
IntegrationTests.test.js
. Much of it is the same as what you already saw in the unit tests, but this time you aren't stubbing anything for data. This is because you do want to test that the flow of data during interactions is happening as expected. However, you should never test against your real database, so the tests have been configured to test with thetest-database.json
file instead. -
Challenge
Integration Testing Continued
The first test is for GET requests to the
/create
endpoint, wherein it checks that it should be getting status code 200OK
for responses and that the text being returned contains the'Create New Product'
text that was passed to the title of the form. The second test, which tests POST requests to that same endpoint, sends an example product to the test database and then checks that it gets a status code 302FOUND
, redirected to the home page at the'/'
route, and that the product added to the test database is the one was passed in. You can run either of these tests just like before with eithernpm test -- -g 'GET create'
ornpm test -- -g 'POST create'
and they should pass.
Test Implementations
The
GET update
andPOST update
test cases for the/:id/update
endpoints will be for you to implement.Instructions (1)
* For the `GET update` test, make sure to check for a `200` status code right after the `response`. * Then check that `response.text` contains the lines `'Update Product'`, `'value=\"Product 2\"'`, and `'value=\"3\"'`. These are the expected values that should fill in the form when updating the product, as the product to be testing has the `id` of 2. * You can check for these lines in 3 different `expect()` calls using `to.include()`. * The `POST update` test flows almost the same logic as `POST create`, except remember that updating a product should not change the size of the data array and that the ID's are indexed at 1, not 0.
The same should apply to the
'POST delete'
test.Instructions (2)
* Make sure to check for a `302` status code after the response, that the `response` was redirected to the `'/'` route for the home page, and that the database no longer has the deleted item. * This would mean that the data array would be 1 less from the original size, and you could further check by using the `find()` function to look for a product with the the deleted products `id`. * If it was deleted, `find()` should give you an `undefined`. * At this point in time, all these tests should fail because you have not yet implemented the routes and route handlers.
Route Implementations
First, define the routes in
products.js
. In a similar vein to the existing GET and POST requests to'/create'
, create routes for GET and POST requests to'/:id/update'
as well as a POST request to'/:id/delete'
all with their respective route handlers.Now return to
ProductController.js
and implement the last 3 route handlers.Instructions (3)
* For `product_update_get()`, simply call `res.render()` the same way as `product_create_get()`, except you should pass in `Update Product` as the `title` along with `productToUpdate` as the second property in the object. * For `product_update_post()`, pass in `productToUpdate` to `updateProduct()` in the same way as seen in `product_create_get()`. * Then redirect back to home with `'/'`. Do the same for `product_delete_post()`. * If done correctly, running the integration tests you just implemented should pass. If not, try and see what the test feedback returns to troubleshoot. -
Challenge
Wrapping Up
If all your tests are now passing, you can run the application with
npm run serverstart
and check out the web browser.Remember you can always reset the data in your
main-database.json
, which is what the application serves, usingnpm run seedItems
. Now that the lab is complete, feel free to experiment with the tests or the application itself.
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.