RESTful Node.js: A Structured Approach
  • Book Cover
  • About the Author
  • Links and Resources
  • Part I: The Why
    • Foreword
    • Preface
    • Chapter 1: Introduction
      • The Rise of REST and Distributed Systems
      • Problem #1: Structureless Design, Structureless REST
      • The Emergence of JavaScript and Node.js
      • Problem #2: Structureless JavaScript, Structureless Node.js
      • Behold, the Solution: A Structured Approach
      • Summary
  • Part 2: The Theory
    • Chapter 2: REST Origins
      • A Brief History of the Web and the Birth of REST
      • REST vs. HTTP
      • REST - The Abstract Web Architecture
      • HTTP - A Peak at REST's Concrete Implementation
      • What does it mean for an API to be RESTful?
      • Measuring "RESTfulness" with Richardson Maturity Model
      • Pragmatic REST vs Dogmatic REST
      • Summary
    • Chapter 3: RESTful API Design Guidelines and "Best Practices"
      • Theories vs. Principles vs. Guidelines
      • URI Design
      • Method Verbs
      • Status Codes
      • Representational Design
      • Metadata Design
      • Versioning Strategies
      • Security Considerations
      • Documentation
      • Case Study: GitHub
      • Summary
    • Chapter 4: Structured JavaScript Architecture
      • The Monstrous Monolith and Its Downfall
      • Layered/N-Tier Architecture: The Unpopular Proven Way
      • Microservices and Distributed Computing: A Popular Misdirection
      • Summary
    • Chapter 5: The 8 Step Recipe
      • Route Name (URI)
      • Input Request
      • Middleware
      • Validation
      • Domain
      • Events
      • Output Response
      • Test, Refactor, Document
      • Summary
  • Part 3: The Code
    • Chapter 6: Introduction to the Bookstore API
      • The Bookstore API Endpoint Specifications
      • API Design and Code Structure
      • Project Setup
      • Summary
    • Chapter 7: Retrieving Books from our API
      • Retrieving All Books - Planning
      • Retrieving All Books - Implementation
      • Retrieving A Book By ID - Planning
      • Retrieving A Book By ID - Implementation
      • Summary
    • Chapter 8: Adding Authentication to our API
      • Registering the User - Planning
      • Registering the User - Implementation
      • Logging the User In - Planning
      • Logging the User In - Implementation
      • Getting Authenticated User - Planning
      • Getting Authenticated User - Implementation
      • Summary
    • Chapter 9: Adding the Create, Update, and Delete Operations to our API
      • Creating A Book Listing - Planning
      • Creating A Book Listing - Implementation
      • Updating A Book Listing By ID - Planning
      • Updating A Book Listing By ID - Implementation
      • Deleting A Book Listing By ID - Planning
      • Deleting A Book Listing By ID - Implementation
      • Summary
    • Chapter 10: Testing our API
      • Testing the Request
      • Testing the Middleware
      • Testing the Validation
      • Testing the Domain
      • Testing the Event
      • Testing the Response
      • Testing the Controller
      • Integration Test
      • Summary
  • Conclusion
    • Final Words
  • Bonus!
    • Refactoring to HATEOAS
  • Appendix
    • Sources & References
Powered by GitBook
On this page
  • Repository
  • Service
Edit on GitHub
  1. Part 3: The Code
  2. Chapter 10: Testing our API

Testing the Domain

Repository

Recall how we implemented the userRepository in one of the earlier chapters.

File: src/domain/repositories/user.repository.js

const UserModel = require('../models/user.model')

/**
 *
 * @param {*} user {
 *  - name
 *  - email
 *  - password
 * }
 *
 * @returns user
 */
const createUser = async (userData) => {
  const user = new UserModel(userData)
  const userReturn = await user.save(userData)

  return userReturn
}

module.exports = {
  createUser
}

Now it's time to write out test cases.

For testing repositories, what we are mainly interested in is if the database operations are working. For that to work, we'll have to arrange to connect to the database on every test.

Here we have a couple of helpers that will help us test for database-based test cases. The db function let's us create a connection to the database, it's simply a wrapper around the mongoose database driver, that is why you see a disconnect function in the afterAll function. We then have a dbTestUtils object that has a clearDatabase function which allows us to reset the database and let's us start off from a clean slate. For a closer look at what those helper functions do, you can dive deeper by looking at the source code of the repository.

File: src/domain/repositories/__tests__/user.repository.test.js

const db = require('../../../utils/db')
let dbConnection
const dbTestUtils = require('../../../../tests/testUtils/dbTestUtil')

const userRepository = require('../user.repository')

beforeAll(async () => {
  dbConnection = await db()
})

afterEach(async () => {
  await dbTestUtils.clearDatabase()
})

afterAll(async () => {
  await dbConnection.disconnect()
})

describe('Test Suite: User Repository', () => {
  // Our tests go here...
})

Now Moving on to the first successful test case, we have the following. We are simply calling the createUser method and passing in all of the necessary fields. This is a nice, clean, and simple test.

test('User Repository - createUser - success', async () => {
  const testUser = {
    first_name: 'Yichen',
    last_name: 'Zhu',
    email: 'yichen@yichen.com',
    password: 'password123',
    phone_number: '1234567890'
  }

  const user = await userRepository.createUser(testUser)
  const expectedName = 'Yichen'
  const actual = user.first_name

  expect(actual).toEqual(expectedName)
})

For the failing test case, we can decide to not pass in certain required fields. Recall that our UserModel had certain required fields when we created it.

Here is what a possible failing test case would look like.

test('User Repository - createUser - error', async () => {
  try {
    const testUser = {
      email: 'yichen@yichen.com'
    }

    await userRepository.createUser(testUser)
  } catch (error) {
    /* eslint-disable-next-line */
    expect(error.message).toBe(
      'user validation failed: phone_number: Path `phone_number` is required., password: Path `password` is required., first_name: Path `first_name` is required.'
    )
  }
})

Although this seems a little redundant due to the fact that we already have validation in another layer, this is simply to illustration how one might go about writing tests for a repository layer. When you start creating more complicated repositories that use multiple different models, you'll find more complicated tests are needed.

Service

Recall how we implemented the authService in one of the earlier chapters.

File: src/domain/services/auth.service.js

const ApiException = require('../../utils/ApiException')
const userRepository = require('../repositories/user.repository')

/**
 * @returns User
 */
const registerUser = async (user) => {
  try {
    const createdUser = await userRepository.createUser(user)
    return createdUser
  } catch (err) {
    if (err.code === 11000 && err?.keyPattern?.email === 1) {
      throw new ApiException({
        message: `This email is already taken.`,
        status: 'error',
        code: 409,
        data: null,
        errors: [`This email is already taken.`]
      })
    }
  }
}

The service layer is going to be all about us mocking out the repository layer. We do so in order to keep the tests as separate and isolated as possible.

For a successful test case, take a look at the following. We do a simple mock of both the userRepository and its method createUser functions and see if they've been called properly.

File: src/domain/services/__tests__/auth.service.test.js

const userRepository = require('../../repositories/user.repository')
const authService = require('../auth.service')

beforeAll(async () => {})

beforeEach(() => {
  userRepository.createUser = jest.fn(() => {
    return {}
  })
})

afterEach(async () => {})

afterAll(async () => {})

describe('Test Suite: Auth Service', () => {
  test('Auth Service - registerUser', async () => {
    const testUser = {
      first_name: 'john',
      last_name: 'doe',
      email: 'john@john.com',
      password: 'password',
      phone_number: '4168561988'
    }
    await authService.registerUser(testUser)

    expect(userRepository.createUser).toHaveBeenCalledWith(testUser)
    expect(userRepository.createUser).toHaveBeenCalledTimes(1)
    expect(userRepository.createUser).toHaveReturnedWith({})
  })
})

For the failing test, we'll simply want to throw an ApiException artificially.

test('Auth Service - registerUser - error', async () => {
  userRepository.createUser = jest.fn(() => {
    class CustomError extends Error {
      constructor() {
        super()
        this.code = 11000
        this.keyPattern = {
          email: 1
        }
      }
    }

    throw new CustomError()
  })

  try {
    await authService.registerUser({})
  } catch (err) {
    /* eslint-disable-next-line */
    expect(err.message).toBe('This email is already taken.')
  }
})
PreviousTesting the ValidationNextTesting the Event

Last updated 3 years ago