Builder Book logo

Book: Builder Book

  1. Introduction
  2. Set up Node.js project. VS code editor and lint. Set up Next.js project. Material-UI integration. Server-side rendering. Custom styles.
  3. HTTP. Express server. Next-Express server, nodemon. Index.getInitialProps. User data model and mongoose. MongoDB database and dotenv. Testing server-database connection. Retrieving document. Session and cookie. MenuWithAvatar and Header components.
  4. Authentication HOC. getInitialProps method. Login page and NProgress. Asynchronous execution. Promise.then. async/await. Google Oauth API infrastructure. setupGoogle, verify, passport, strategy. Express routes /auth/google, /oauth2callback, /logout. generateSlug. this. Set up at Google Cloud Platform.
  5. 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.
  6. Book data model. Chapter data model. MongoDB index. API infrastructure and user roles. Read chapter API.
  7. Set up Github API infrastructure. Sync content API infrastructure. Missing UI infrastructure for Admin user. Two improvements. Testing.
  8. Table of Contents. Sections. Sidebar. Toggle TOC. Highlight for section. Active section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book API infrastructure. Setup at Stripe dashboard and environmental variables. isPurchased and ReadChapter page. Redirect. My books API and MyBooks page. Mailchimp API.
  10. Prepare project for deployment. Environmental variables, production/development. Logger. SEO, robots.txt, sitemap.xml. Compression and security. Deploy project. Heroku. Testing deployed project. AWS Elastic Beanstalk.

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.

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.


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?

  1. Scenario 1. We should test if the generateSlug method generates the proper value for slug when no user exists with the same slug in the corresponding collection inside our database. In this scenario, we basically test if slugify converts name into slug using the kebabCase method. Let's call this test no duplication. Check up the Lodash docs about how kebabCase works: https://devdocs.io/lodash~4/index#kebabCase

  2. Scenario 2. We should test if the generateSlug method generates the proper value for slug when a user with the same slug value already exists in our database. Let's call this test one duplication.

  3. Scenario 3. Optionally, we should test if the generateSlug method generates the proper value for slug when a user with the same slug value already exists and another user with that slug value originalSlugValue-1 exists as well. We'll call this test multiple 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.

  1. Example 1. Take a look at the no duplication test. Remember that the generateSlug method generates a slug with the value john-johnson for a MockUser with the name value of John Johnson. Since john-johnson does not match any value from the array slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john'], MockUser returns Promise.resolve(null). This means that in our "database", there is no user with the john-johnson slug. Thus, the following code gets executed inside the generateSlug method:
    if (!user) {
     return origSlug;
    }
    If the user does not exist, the origSlug value is indeed original and becomes our user's slug.
  2. Example 2. Take a look at the next test, one duplication. The generateSlug method generates a slug with value john from the name with value John. Since john does match a value in the array slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john'], instead of Promise.resolve(null), MockUser returns Promise.resolve({ id: 'id' }). Thus, the following code inside the generateSlug method gets executed:
    return createUniqueSlug(Model, origSlug, 1);
    origSlug is not original, so the createUniqueSlug() function adds -1 to the john slug, thereby outputting john-1 as a value for slug.

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:
Builder Book

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:
Builder Book

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.

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.