Chapter 4: Testing method with Jest. Transactional email API with AWS SES service. Set up AWS SES service, security credentials. sendEmail method. Export and import syntax for server code. EmailTemplate data model. Update User.signInOrSignUp. Informational success/error messages. Notifier component. notify method.
The section below is a preview of Builder Book. To read the full text, you will need to purchase the book.
In Chapter 4, you'll start with the codebase in the 4-begin folder of our builderbook repo and end up with the codebase in the 4-end folder. We'll cover the following topics in this chapter:
Testing method with Jest
Transactional email API with AWS SES service - Set up AWS SES service, security credentials - sendEmail method - Export and import syntax for server code - EmailTemplate data model, insertTemplates and getEmailTemplate methods - Update User.signInOrSignUp - Testing
Informational success/error messages - Notifier component - notify method - Example of usage
In the previous three chapters, we: - Set up and integrated our Next.js web app with Material-UI's library, set up ESLint, and built our first page, component, and HOC (Chapter 1). - Created our Express server, discussed session and cookie, connected our server to MongoDB database, and defined our User
data model (Chapter 2). - Added a withAuth
HOC, discussed Promise and async/await
usage, and built Google OAuth API (Chapter 3).
In this chapter (Chapter 4), our main goals are: - Learn how to use Jest to test a simple method - Build Transactional email API using AWS SES service - Add so-called informational success/error messages to our web application.
Testing method with Jest link
As our project grows, it takes more time to test our code. At some point, if you codebase gets large and hard to read, you may start replacing manual testing with less time-consuming automatic testing. Although the final project in this book is still not large enough to justify writing and maintaining any automatic tests, we think it's a good idea to show how a simple method, generateSlug
, can be automatically tested by the Jest library. You can write a test as small as checking one method or as complex as checking a cascade of events (for example, an integration test that reviews an entire API infrastructure with multiple request-response cycles, like Google OAuth API infrastructure). Here's more on the types of tests you can create:
http://www.continuousagile.com/unblock/test_types.html
In this section, we get familiar with automated code testing. As a simple example, we will test if our method that generates the proper slug
out of name
in different scenarios.
We placed our slug-generating method generateSlug
at server/utils/slugify.js
. You may remember from Chapter 3 that we used generateSlug
to generate the value for slug
from the value of displayName
of our User
data model. We will also use this method to generate a slug
for the Book
and Chapter
data models. We will use the generateSlug
method when defining the Book.add
static method inside Chapter 5. We will then use generateSlug
when defining the Chapter.syncContent
static method inside Chapter 6.
We don't want generateSlug
to mess up creation of slug
values - we want all Book
and Chapter
MongoDB documents to have unique values for their slug
field. If generateSlug
does not work as expected, not only will we render the wrong content on our website, but we will also provide the wrong information to search engine bots (SEO).
We will use Jest, a popular JavaScript testing library by Facebook. We will write a simple unit test that checks if the generateSlug
method generates the proper value of slug
.
First, we need to decide where to place our unit test. Jest automatically finds files containing test code. We have to either place our files in a __tests__
folder or create a file with a .test.js
extension. We use the latter option - for no strong reason.
Below, we will create a slugify.test.js
file to test our generateSLug
method from server/utils/slugify.js
. We place the test file into test/server/utils/slugify.test.js
. Go ahead and create new folders and the test file itself.
To design our unit test, we should read and understand the definition of generateSlug
inside our server/utils/slugify.js
file. How does this method work, what does it take as an input, and what does it return as an output? Open server/utils/slugify.js
:
const _ = require('lodash');
// https://devdocs.io/lodash~4/index#kebabCase
const slugify = (text) => _.kebabCase(text);
async function createUniqueSlug(Model, slug, count) {
const user = await Model.findOne({ slug: `${slug}-${count}` }, 'id');
if (!user) {
return `${slug}-${count}`;
}
return createUniqueSlug(Model, slug, count + 1);
}
async function generateSlug(Model, name, filter = {}) {
const origSlug = slugify(name);
const user = await Model.findOne({ slug: origSlug, ...filter }, 'id');
if (!user) {
return origSlug;
}
return createUniqueSlug(Model, origSlug, 1);
}
module.exports = generateSlug;
The generateSlug
method does a few things: - First, the method converts name
into origSlug
using the helper method slugify
, which is basically Lodash's library method kebabCase
. - Then generateSlug
searches our database for a MongoDB document using origSlug
as a value for the slug
field and whatever filter
options we might have passed as arguments to generateSlug
. - If a user with the same slug: origSlug
does not exist, then our method returns the value of origSlug
. - If a user with the same slug does exist, then our method adds a dash and number at the end of the origSlug
value by calling the helper method createUniqueSlug
.
Ok, we understand what the generateSlug
method does. It generates slug
from the name
and makes sure that all MongoDB documents within a collection have unique values for the slug
field by adding -1
, -2
and so on to the original origSlug
value.
Based on the above understanding, what are the scenarios in which we need to test out the generateSlug
method?
Scenario 1. We should test if the
generateSlug
method generates the proper value forslug
when no user exists with the sameslug
in the corresponding collection inside our database. In this scenario, we basically test ifslugify
convertsname
intoslug
using thekebabCase
method. Let's call this testno duplication
. Check up the Lodash docs about howkebabCase
works: https://devdocs.io/lodash~4/index#kebabCaseScenario 2. We should test if the
generateSlug
method generates the proper value forslug
when a user with the sameslug
value already exists in our database. Let's call this testone duplication
.Scenario 3. Optionally, we should test if the
generateSlug
method generates the proper value forslug
when a user with the sameslug
value already exists and another user with thatslug
valueoriginalSlugValue-1
exists as well. We'll call this testmultiple duplications
.
Let's follow the example in Jest's docs on how to write a unit test for a simple function (method). Here is a simple function:
function sum(a, b) {
return a + b;
}
export default sum
For this function, the sum checker
test is:
import sum from './sum';
test('sum checker', () => {
expect(sum(1, 2)).toBe(3);
});
Following the example above, for the test no duplication
and function generateSlug()
, we will get:
test('no duplication', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John Jonhson').then((slug) => {
expect(slug).toBe('john-jonhson');
});
});
This test says that for a user with name
John Jonhson
, the test expects a slug
with value john-jonhson
.
The only unknown code in the block above is expect.assertions(1)
. The number of assertions is the number of expect().toBe()
assertions that you expect inside the test.
In our case, the number of assertions is one. By specifying expect.assertions(1);
, we tell Jest to call that one assertion that we have in the code expect(slug).toBe('john-jonhson');
. Without specifying expect.assertions(1);
, our assertion may or may not be called. If we specify the wrong number of assertions, our test will fail. When the number of assertions is 2 or more, use expect.assertions(number)
to make sure that all assertions are called. You can read more about expect.assertions(number)
usage in the Jest docs:
https://facebook.github.io/jest/docs/en/expect.html#expectassertionsnumber
You just wrote one test - no duplication
. Next, let's write our two other tests - one duplication
and multiple duplications
- and put all three of them into the so-called test suite by using Jest's describe(name, fn)
syntax:
https://facebook.github.io/jest/docs/en/api.html#describename-fn
Open your test/server/utils/slugify.test.js
file and add the following content to it:
const generateSlug = require('../../../server/utils/slugify');
const MockUser = {
slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john'],
findOne({ slug }) {
if (this.slugs.includes(slug)) {
return Promise.resolve({ id: 'id' });
}
return Promise.resolve(null);
},
};
describe('slugify', () => {
test('no duplication', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John Jonhson.').then((slug) => {
expect(slug).toBe('john-jonhson');
});
});
test('one duplication', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John.').then((slug) => {
expect(slug).toBe('john-1');
});
});
test('multiple duplications', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John Jonhson Jr.').then((slug) => {
expect(slug).toBe('john-jonhson-jr-2');
});
});
});
Let's understand the logic inside the above definition of MockUser
: - MockUser
returns a user if the generated slug
does match a value from the array slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
. - MockUser
returns null
if the generated slug
does not match any value from the array slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
.
Think of this slugs
array as an imitation of our MongoDB database - our "database" has 3 users with the slugs john-jonhson-jr
, john-jonhson-jr-1
, and john
. This setup for a "database" is not straightforward to understand, so let's discuss two examples.
- Example 1. Take a look at the
no duplication
test. Remember that thegenerateSlug
method generates aslug
with the valuejohn-johnson
for aMockUser
with thename
value ofJohn Johnson
. Sincejohn-johnson
does not match any value from the arrayslugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
,MockUser
returnsPromise.resolve(null)
. This means that in our "database", there is no user with thejohn-johnson
slug. Thus, the following code gets executed inside thegenerateSlug
method:
If the user does not exist, theif (!user) { return origSlug; }
origSlug
value is indeed original and becomes our user'sslug
. - Example 2. Take a look at the next test,
one duplication
. ThegenerateSlug
method generates a slug with valuejohn
from thename
with valueJohn
. Sincejohn
does match a value in the arrayslugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
, instead ofPromise.resolve(null)
,MockUser
returnsPromise.resolve({ id: 'id' })
. Thus, the following code inside thegenerateSlug
method gets executed:return createUniqueSlug(Model, origSlug, 1);
origSlug
is not original, so thecreateUniqueSlug()
function adds-1
to thejohn
slug, thereby outputtingjohn-1
as a value forslug
.
Alright, now that you know how our new tests work - it's time for actual automatic testing of the generateSlug
method.
To run Jest, add this extra script command to our scripts
section inside the package.json
file:
"test": "jest"
Go to your terminal, navigate to the book/4-begin
folder (your project folder), and run yarn test
. Jest will generate the following report:
From this Jest report, we can see that our test suite called slugify
passed. This suite consists of three tests: no duplication
, one duplication
, and multiple duplications
.
To see how the report looks when the test fails, go to server/utils/slugify.js
. Find line 24:
return createUniqueSlug(Model, slug, count + 1);
Temporarily modify this line to become:
return createUniqueSlug(Model, slug, count + 2);
Go to your terminal and run yarn test
again in your working project folder. Jest will generate a new report:
This report shows that the third test called multiple duplications
failed. As a result, the entire test suite failed. If at least one test within a suite fails, the entire test suite fails. Jest specifies the reason for failure:
Expected value to be (using ===):
"john-jonhson-jr-2"
Received:
"john-jonhson-jr-3"
In this case, our generateSlug
method failed the test because it generated john-jonhson-jr-3
instead of john-jonhson-jr-2
. Don't forget to go back to server/utils/slugify.js
and undo our temporary edit to the code.
One final note on Jest - saving reports is easy. We simply modify package.json
as follows. Edit the test
script command to become:
"test": "jest --coverage"
You've reached the end of the Chapter 4 preview. To continue reading, you will need to purchase the book.