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.')
  }
})

Last updated