Logo

dev-resources.site

for different kinds of informations.

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

Published at
1/7/2023
Categories
unittesting
integrationtesting
testing
typescript
Author
mattwilliams
Author
12 person written this
mattwilliams
open
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!

integrationtesting Article's
30 articles in total
Favicon
Top 7 Best Integration Testing Tools for 2025
Favicon
Unit Test vs. Integration Test
Favicon
Choose Boring Releases
Favicon
The Struggle for Microservice Integration Testing
Favicon
Integration Testing in React: Best Practices with Testing Library
Favicon
Integration Testing in .NET: A Practical Guide to Tools and Techniques
Favicon
Testing Modular Monoliths: System Integration Testing
Favicon
Integration Testing : Concept
Favicon
The Complete Guide to Integration Testing
Favicon
End to End Testing vs Integration Testing โ€“ 7 Key Differences
Favicon
How to improve test isolation in integration testing using Jest and testcontainers
Favicon
๐Ÿš€ Effortless Integration Tests with Testcontainers in Golang ๐Ÿงช
Favicon
Testcontainers - Integration Testing Using Docker In .NET
Favicon
Simple Mocks With Mockaco
Favicon
Ace Your Tests with Mocha, Chai, Sinon, and the Clean Architecture
Favicon
Testing Made Easy with Ava: No More Pulling Out Your Hair!
Favicon
Nock: The Purr-fect Tool for Testing HTTP Interactions!
Favicon
Take Your Mocktail Game to the Next Level with Sinon.js: The Fun Way to Test Your Code
Favicon
Get Your Chai Fix and Learn the Art of Assertions at the Same Time
Favicon
Mocha Testing: The Java in Your Script
Favicon
The ultimate guide to Jest: turn your testing woes into rainbows and unicorns
Favicon
Get Your Test On with Jasmine: The Happy Way to Ensure Quality
Favicon
Experience the joy of stress-free coding with unit and integration testing!
Favicon
Boost Your Productivity with These Top-Notch JavaScript Testing Libraries! (2022)
Favicon
What is Integration Testing?
Favicon
Automated integration testing - what features help?
Favicon
When should integration testing be automated?
Favicon
Azure Functions E2E Testing within GitHub Actions
Favicon
Differentiate between integration testing and system testing
Favicon
Beginners' Introduction to React Testing

Featured ones: