dev-resources.site
for different kinds of informations.
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,
) {}
}
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;
}
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);
}
}
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;
}
}
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);
});
});
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,
});
});
});
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!
Featured ones: