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
generateSlugmethod generates the proper value forslugwhen no user exists with the sameslugin the corresponding collection inside our database. In this scenario, we basically test ifslugifyconvertsnameintoslugusing thekebabCasemethod. Let's call this testno duplication. Check up the Lodash docs about howkebabCaseworks: https://devdocs.io/lodash~4/index#kebabCaseScenario 2. We should test if the
generateSlugmethod generates the proper value forslugwhen a user with the sameslugvalue already exists in our database. Let's call this testone duplication.Scenario 3. Optionally, we should test if the
generateSlugmethod generates the proper value forslugwhen a user with the sameslugvalue already exists and another user with thatslugvalueoriginalSlugValue-1exists 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 sumFor 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 duplicationtest. Remember that thegenerateSlugmethod generates aslugwith the valuejohn-johnsonfor aMockUserwith thenamevalue ofJohn Johnson. Sincejohn-johnsondoes not match any value from the arrayslugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john'],MockUserreturnsPromise.resolve(null). This means that in our "database", there is no user with thejohn-johnsonslug. Thus, the following code gets executed inside thegenerateSlugmethod:If the user does not exist, theif (!user) { return origSlug; }origSlugvalue is indeed original and becomes our user'sslug. - Example 2. Take a look at the next test,
one duplication. ThegenerateSlugmethod generates a slug with valuejohnfrom thenamewith valueJohn. Sincejohndoes match a value in the arrayslugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john'], instead ofPromise.resolve(null),MockUserreturnsPromise.resolve({ id: 'id' }). Thus, the following code inside thegenerateSlugmethod gets executed:return createUniqueSlug(Model, origSlug, 1);origSlugis not original, so thecreateUniqueSlug()function adds-1to thejohnslug, thereby outputtingjohn-1as 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.