DEV Community

Matt Williams for Tech Dev Blog

Posted on • Originally published at techdevblog.io on

Ace Your Tests with Mocha, Chai, Sinon, and the Clean Architecture

Ace Your Tests with Mocha, Chai, Sinon, and the Clean Architecture

Hey there! Are you ready to learn how to test a backend like a pro? Well, you're in the right place! In this tutorial, we'll be using TypeScript, DynamoDB, Mocha, Chai, and Sinon.js to build and test a to-do list app's backend following the Clean Architecture principles.

First things first, let's make sure you have everything you need to get started. If you don't already have them installed, you'll need to install TypeScript, the AWS SDK, Mocha, Chai, and Sinon.js. You'll also need to set up an AWS account and create a DynamoDB table to use as the backend for your to-do list app.

With all that out of the way, let's get started!

Creating the backend

Architecture

First, let's define the architecture of our to-do list app. We'll be following the Clean Architecture principles, which means that our backend will be divided into three layers: the Entities layer, the Use Cases layer, and the Adapters layer.

The Entities layer contains our core business logic and data structures. In our case, this will include a Task class that represents a single to-do list item, and a TaskRepository interface that defines the methods we'll use to interact with our DynamoDB table.

The Use Cases layer contains our application-specific logic. This is where we'll define the business rules for our to-do list app, such as how to add, remove, and update tasks.

Finally, the Adapters layer contains the code that communicates with the outside world, such as our DynamoDB table and any external APIs we might be using.

Implementation

Now that we have a high-level understanding of our architecture, let's start implementing it. We'll start by defining our Task class in the Entities layer:

// src/entities/task.ts

export class Task {
  constructor(
    public readonly id: string,
    public readonly title: string,
    public readonly description: string,
    public readonly dueDate: string,
  ) {}
}

Enter fullscreen mode Exit fullscreen mode

Next, we'll define our TaskRepository interface in the Entities layer:

// src/entities/task-repository.ts

export interface TaskRepository {
  create(task: Task): Promise;
  delete(id: string): Promise;
  find(id: string): Promise;
  list(): Promise;
  update(task: Task): Promise;
}

Enter fullscreen mode Exit fullscreen mode

Now that we have our Entities layer set up, let's move on to the Use Cases layer. Here, we'll define a CreateTaskUseCase class that handles the business logic for creating a new task:

// src/use-cases/create-task-use-case.ts

import { Task } from '../entities/task';
import { TaskRepository } from '../entities/task-repository';

export class CreateTaskUseCase {
  constructor(private readonly taskRepository: TaskRepository) {}

  async execute(title: string, description: string, dueDate: string): Promise {
    const task = new Task(uuidv4(), title, description, dueDate);
    return this.taskRepository.create(task);
  }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, our CreateTaskUseCase class takes a TaskRepository as an argument in its constructor, and it has a single method called execute that creates a new Task object and saves it to the repository.

Now that we've implemented our Use Cases layer, let's move on to the Adapters layer. Here, we'll create a DynamoDBTaskRepository class that implements our TaskRepository interface and communicates with our DynamoDB table:

// src/adapters/dynamodb-task-repository.ts

import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { Task } from '../entities/task';
import { TaskRepository } from '../entities/task-repository';

export class DynamoDBTaskRepository implements TaskRepository {
  constructor(
    private readonly documentClient: DocumentClient,
    private readonly tableName: string,
  ) {}

  async create(task: Task): Promise {
    await this.documentClient
      .put({
        TableName: this.tableName,
        Item: task,
      })
      .promise();
    return task;
  }

  async delete(id: string): Promise {
    await this.documentClient
      .delete({
        TableName: this.tableName,
        Key: { id },
      })
      .promise();
  }

  async find(id: string): Promise {
    const result = await this.documentClient
      .get({
        TableName: this.tableName,
        Key: { id },
      })
      .promise();
    return result.Item as Task | undefined;
  }

  async list(): Promise {
    const result = await this.documentClient
      .scan({
        TableName: this.tableName,
      })
      .promise();
    return result.Items as Task[];
  }

  async update(task: Task): Promise {
    await this.documentClient
      .update({
        TableName: this.tableName,
        Key: { id: task.id },
        UpdateExpression: 'set title = :t, description = :d, dueDate = :dd',
        ExpressionAttributeValues: {
          ':t': task.title,
          ':d': task.description,
          ':dd': task.dueDate,
        },
        ReturnValues: 'ALL_NEW',
      })
      .promise();
    return task;
  }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, our DynamoDBTaskRepository class uses the AWS SDK's DocumentClient to communicate with our DynamoDB table and implements all of the methods defined in our TaskRepository interface.

Testing the backend

Now that we've implemented our backend, let's move on to testing it. We'll start by writing some unit tests for our Use Cases layer using Mocha and Chai.

First, let's write a test for our CreateTaskUseCase class. We'll use Sinon.js to mock our DynamoDBTaskRepository so that we can control the behaviour of our tests:

// test/use-cases/create-task-use-case.test.ts

import { CreateTaskUseCase } from '../../src/use-cases/create-task-use-case';
import { DynamoDBTaskRepository } from '../../src/adapters/dynamodb-task-repository';
import { Task } from '../../src/entities/task';
import { expect } from 'chai';
import * as sinon from 'sinon';

describe('CreateTaskUseCase', function() {
  it('creates a new task and saves it to the repository', async function() {
    // Arrange
    const taskRepository = sinon.createStubInstance(DynamoDBTaskRepository);
    const createTaskUseCase = new CreateTaskUseCase(taskRepository);
    const title = 'Buy milk';
    const description = 'Remember to buy milk on the way home';
    const dueDate = '2022-01-01';
    const task = new Task('123', title, description, dueDate);
    taskRepository.create.resolves(task);

    // Act
    const result = await createTaskUseCase.execute(title, description, dueDate);

    // Assert
    expect(result).to.deep.equal(task);
    expect(taskRepository.create).to.have.been.calledOnceWithExactly(task);
  });
});

Enter fullscreen mode Exit fullscreen mode

In this test, we use Sinon's createStubInstance function to create a mock TaskRepository object. We then use Sinon's resolves method to specify the behavior of our mock repository's create method. Finally, we use Mocha and Chai to define our test and make assertions about the behavior of our CreateTaskUseCase class.

We can follow a similar pattern to write tests for our other Use Cases and Adapters as well. For example, here's a test for our DynamoDBTaskRepository class:

// test/adapters/dynamodb-task-repository.test.ts

import { DynamoDBTaskRepository } from '../../src/adapters/dynamodb-task-repository';
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { Task } from '../../src/entities/task';
import { expect } from 'chai';
import * as sinon from 'sinon';

describe('DynamoDBTaskRepository', function() {
  it('creates a new task in the DynamoDB table', async function() {
    // Arrange
    const documentClient = sinon.createStubInstance(DocumentClient);
    const tableName = 'tasks';
    const taskRepository = new DynamoDBTaskRepository(documentClient, tableName);
    const task = new Task('123', 'Buy milk', 'Remember to buy milk on the way home', '2022-01-01');
    documentClient.put.resolves({});

    // Act
    await taskRepository.create(task);

    // Assert
    expect(documentClient.put).to.have.been.calledOnceWithExactly({
      TableName: tableName,
      Item: task,
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

Conclusion

As you can see, testing our backend using Mocha, Chai, and Sinon.js is easy and intuitive. With just a few lines of code, we can write comprehensive tests that ensure our Use Cases and Adapters are working as expected.

Of course, this is just a small example of what these libraries can do. You can use Mocha, Chai, and Sinon.js to test all sorts of scenarios and edge cases, from testing error handling to testing performance. And with TypeScript, and AWS DynamoDB, you have all the tools you need to build and deploy a scalable, reliable backend for your to-do list app.

So go ahead and give it a try! And remember to delete your DynamoDB table when you're done. Happy testing!

Top comments (0)