ExpressJS and Sequelize application tested with mocha, chai, supertest, migrations and seeds
In this demo, we will explore one way to test your expressJS + Sequelize application without risking your database, and making sure that you have enough test data to cover not only common test cases, but also edge test cases.
Initializing Sequelize + ExpressJS application
Firstly, we will start by creating a new Sequelize project. In this example I’ll be using Node v14.15.0, and MySQL 5.7.29, but this solution could perfectly work with any database.
We will start by installing and initializing Sequelize, ExpressJS and sequelize-cli as the core tools for developing the application.
npm init
npm install --save sequelize
npm install --save mysql2
npm install --save express
In addition, we will install mocha and chai tools for testing and assertion, and supertest as one of the most frequently used options to test express applications.
npm install --save mocha
npm install --save supertest
npm install --save chai
Then, we will install some extra useful packages related to configurations. One of them is dotenv (used for loading information from an .ENV
file by accessing process.env
), and the other is cross-env (used for helping us to set the NODE_ENV
in any command line, without worrying about the operating system). Another very useful package to install is the body-parser, a middleware used with express to get request’s body as an Object.
npm install --save dotenv
npm install --save-dev cross-env
npm install --save-dev body-parser
After all needed packages are installed, we will initialize the application by running:
npx sequelize-cli init
Database configuration
Now that we have all needed dependencies installed, let’s proceed to create and configure the database connection. For this example, we will be setting three database configurations: one for production, other for our development environment, and another that will be dynamically created and populated each time you run your tests.
Why are we writing development and testing configuration? Could they not be the same? In my experience doing unit tests, I realized that when I run unit tests, I don’t want to lose my development data because if I use the same database for testing and development, I’ll probably start to lose control of my data. As a developer, I like to keep my data (even in a development environment) secure.
After initializing your app with sequelize-cli, you will find that there is a file called config/config.json
. We will edit this file changing its name to config/config.js
. Then we will set our configuration similar to the following example:
require('dotenv').config();module.exports = {
development: {
username: process.env.MYSQL_DEV_USER,
password: process.env.MYSQL_DEV_PASSWORD,
database: process.env.MYSQL_DEV_DB_NAME,
host: process.env.MYSQL_DEV_HOST,
dialect: 'mysql',
},
test: {
username: process.env.MYSQL_TEST_USER,
password: process.env.MYSQL_TEST_PASSWORD,
database: process.env.MYSQL_TEST_DB_NAME,
host: process.env.MYSQL_TEST_HOST,
port: process.env.MYSQL_TEST_PORT,
dialect: 'mysql',
},
production: {
username: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DB_NAME,
host: process.env.MYSQL_HOST,
dialect: 'mysql',
},
};
To use this config file as a javascript file instead of as a JSON, we need to update
models/index.js
file, changing the config file loaded when the constconfig
is defined .const config = require(__dirname + ‘/../config/config.js’)[env];
We can now create our database with sequelize-cli.
npx sequelize-cli db:create
Creating models
For this example, we will imagine that we are creating products for an inventory application. This products will have name, price, stock and minStock. After defining our model, we will run the migration to create this table on the database.
npx sequelize model:generate --name Product --attributes name:string,price:double,stock:integer
After running this command, you will see two new files in your project, one for the migration and one for the model. Now we will run the migration in order to create our products
table in the dev database.
npx sequelize-cli db:migrate
I recommend you to add some products directly from your database, in order to have some values to difference the ones from development with the ones from testing.
Create basic endpoint
Now that we have our database created, we will create a simple endpoint to fetching products, and also get an alert with the value true
if the stock is less than the minStock
value.
For doing this, first we will create three important files, app.js
as our entry point to the system, a router file, and a controller (in some software architectures, a controller is an entity in charge of interacting between human actions and business logic). Create the following files:
routes/index.js
app.js
controllers/products.js
If everything it’s OK, you should check the result of this in your browser, or with any tool like postman or insomnia. In order to do that, just make a GET
request to http://localhost:3030/products. You should get something like this (as I recommended you before, I added one product directly in my dev database):
{
"status": "success",
"data": [
{
"id": 1,
"name": "Item 1",
"price": 100,
"stock": 20,
"minStock": 10,
"createdAt": "2021-02-08T16:50:26.000Z",
"updatedAt": "2021-02-08T16:50:26.000Z",
"shouldRestock": false
}
]
}
Writing unit tests with seeders
So, what happens now if we want to check if the shouldRestock
alert works, but we don’t want to change anything in our development database, because we want to preserve the data as it is? To solve this problem, we use sequelize seeds with a testing database, only for unit tests.
Let’s start by creating the testing database.
npx cross-env NODE_ENV=test npx sequelize-cli db:create
Now, we will write one seeder to populate the Products
table with useful data.
npx sequelize-cli seed:generate --name test-products
After running this command, a new file will be created in the seeders/
directory. We will then move it to seeders/test
in order to keep separated places for each kind of seeder.
Now let’s write a simple seeder. In this case, we need three different products to cover all stock cases:
- Product A with more than
minStock
- Product B with less than
minStock
- Product C with the same value of
minStock
.
The advantage of having full control in our data is that we can reach full coverage, because we designed our data exactly as we need it to test our app.
Now let’s write a simple test, starting with a new file in test/controller/products.test.js
. In the following example, we will create three different objects with the expected data, and our test case will expect the response to contain the three mentioned objects (using expect
from mocha).
Now again that we have our test written, the final step is to configure the test script. This script should have the following behavior:
- Drop previous testing database
- Create new testing database
- Seed testing database
- Run tests
For doing this, we will write this commands inside the scripts
section in package.json
file.
"test": "cross-env NODE_ENV=test mocha ./test/*",
"pretest": "cross-env NODE_ENV=test npm run db:reset",
"db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all --seeders-path ./seeders/test",
"db:create:test": "cross-env NODE_ENV=test npx sequelize-cli db:create"
Run your tests
Now, we can finally run npm test
and test if our test is working correctly. If everything goes well, you should get a response like this.
Sequelize CLI [Node: 14.15.0, CLI: 6.2.0, ORM: 6.5.0]Loaded configuration file "config/config.js".
Using environment "test".
Database products_test dropped.Sequelize CLI [Node: 14.15.0, CLI: 6.2.0, ORM: 6.5.0]Loaded configuration file "config/config.js".
Using environment "test".
Database products_test created.Sequelize CLI [Node: 14.15.0, CLI: 6.2.0, ORM: 6.5.0]Loaded configuration file "config/config.js".
Using environment "test".
== 20210205141729-create-product: migrating =======
== 20210205141729-create-product: migrated (0.031s)Sequelize CLI [Node: 14.15.0, CLI: 6.2.0, ORM: 6.5.0]Loaded configuration file "config/config.js".
Using environment "test".
== 20210208203854-test-products: migrating =======
== 20210208203854-test-products: migrated (0.010s)> testing_express_sequelize@1.0.0 test /Users/seba/Documents/Teaching/testing_express_sequelize
> cross-env NODE_ENV=test mocha ./test/*Listening on port: 3030Fetch products test
Executing (default): SELECT `id`, `name`, `price`, `stock`, `minStock`, `createdAt`, `updatedAt` FROM `Products` AS `Product`;
✓ Shows all stock states (74ms)1 passing (77ms)
Conclusion
After following this demo, now you have an express application with unit test and custom seeded data. From this example, you can now start creating your own unit tests with seeders. If you want to see the entire project, you can find it on Github.