Avoiding Mocks in Testing Through Single Responsibility and Dependency Injection
2024-02-27
How to Create an Express + TypeORM REST API and Return Data as a Stream
2023-11-20
2024-02-27
2023-11-20
I personally hate to use mocks or spies when I write tests. Often they make tests more fragile, unclear and unreliable. Personally a way that I learned to avoid them is to adhere to principles like Single Responsibility Principle (SRP) and Dependency Injection (DI).Â
One way to reduce the need for mocks is by designing your code with principles like Single Responsibility Principle (SRP) and Dependency Injection (DI). This makes the code more modular, easier to test, and less reliant on mocks.
Let me show you what I am talking about with 2 examples:
Consider a TypeScript class that directly creates and uses an external library or service within its methods. This tight coupling makes it difficult to test without resorting to mocks.
class EmailService {
sendEmail(to: string, subject: string, body: string) {
// Directly using an external email sending library
const emailClient = new ExternalEmailClient();
emailClient.configure();
emailClient.send(to, subject, body);
}
}
To test the bad example, you'd typically need to use a library that supports mocking global objects or constructors, which can be cumbersome and lead to brittle tests. However, TypeScript itself doesn't directly support mocking like some dynamic languages do, so you'd have to rely on a JavaScript testing framework that allows such operations, like Jest.
Here's an illustrative example using Jest to mock ExternalEmailClient: In this example, EmailService is directly dependent on ExternalEmailClient. To test EmailService's sendEmail method, you would have to mock ExternalEmailClient, which complicates the test setup.
// Assume ExternalEmailClient is imported from somewhere
jest.mock('./ExternalEmailClient', () => {
return jest.fn().mockImplementation(() => {
return {
configure: jest.fn(),
send: jest.fn(),
};
});
});
describe('EmailService', () => {
it('should send an email using ExternalEmailClient', () => {
const emailService = new EmailService();
emailService.sendEmail('test@example.com', 'Test Subject', 'Test Body');
// Assertions to verify that ExternalEmailClient was called correctly
// This part is highly dependent on the mocking framework's API
});
});
This approach has several downsides:
A better approach is to refactor the code to follow the Single Responsibility Principle and use Dependency Injection. This way, each class has only one reason to change, and dependencies are injected rather than hard-coded.
interface IEmailClient {
configure(): void;
send(to: string, subject: string, body: string): void;
}
class EmailService {
private emailClient: IEmailClient;
constructor(emailClient: IEmailClient) {
this.emailClient = emailClient;
}
sendEmail(to: string, subject: string, body: string) {
this.emailClient.configure();
this.emailClient.send(to, subject, body);
}
}
class ExternalEmailClient implements IEmailClient {
configure(): void {
// Configuration logic for the external email client
}
send(to: string, subject: string, body: string): void {
// Sending logic for the external email client
}
}
Testing the good example is more straightforward and doesn't typically require special mocking libraries. You can easily create a mock or stub that implements the IEmailClient interface and pass it to the EmailService constructor.
Here's an example using a simple mock object:
class MockEmailClient implements IEmailClient {
configureCalled = false;
sendCalledWith: [string, string, string] | null = null;
configure(): void {
this.configureCalled = true;
}
send(to: string, subject: string, body: string): void {
this.sendCalledWith = [to, subject, body];
}
}
describe('EmailService', () => {
it('should send an email using the provided email client', () => {
const mockEmailClient = new MockEmailClient();
const emailService = new EmailService(mockEmailClient);
emailService.sendEmail('test@example.com', 'Test Subject', 'Test Body');
expect(mockEmailClient.configureCalled).toBe(true);
expect(mockEmailClient.sendCalledWith).toEqual(['test@example.com', 'Test Subject', 'Test Body']);
});
});
This testing approach has several advantages:
The refactored (good) example not only adheres to the Single Responsibility Principle and employs Dependency Injection for better design but also significantly improves testability. Tests become more focused on behavior rather than implementation, are easier to write and understand, and are less brittle.
Recently, I have been learning how to develop REST APIs using Express and how to write tests for them. As part of my learning journey, I have also been exploring TypeScript ORMs capable of handling streams.
A stream is a sequence of data elements made available over time, used for handling large volumes of data or continuous data flow. Streams are useful because they allow for efficient processing of data chunks as soon as they are available, without waiting for the entire data set. This is particularly helpful in managing resources and improving performance in applications like real-time data feeds, file processing, and network communications.
Streams can be particularly useful in various scenarios, such as:
For this purpose, I created the blog-orm repository, which showcases how to interact with a PostgreSQL database using TypeORM and return data as a stream.
For creating the DB Post
table you just need to implement a Post.ts
entity and data-source.ts
:
//entity/Post.ts
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column()
content: string
@Column()
isPublished: boolean
}
//data-source.ts
import "reflect-metadata"
import { DataSource } from "typeorm"
import { Post } from "./entity/Post"
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "postgres",
database: "blog",
synchronize: true,
logging: false,
entities: [Post],
migrations: [],
subscribers: [],
})
A service could look like that:
//postService.ts
import { AppDataSource } from "./data-source";
import { Post } from "./entity/Post";
export async function getAllStream() {
return await AppDataSource.getRepository(Post).createQueryBuilder("post").stream();
}
And finally the controller:
//controller.ts
import { Request, Response, NextFunction } from "express";
import * as postService from "./postService"
import { pipeline } from "stream/promises";
import Disassembler from 'stream-json/Disassembler';
import Stringer from 'stream-json/Stringer';
export async function getAllStream(req: Request, res: Response, next: NextFunction) {
try {
const stream = await postService.getAllStream();
res.setHeader('Content-Type', 'application/json');
const tokenizer = new Disassembler();
const jsonStream = new Stringer({ makeArray: true });
pipeline(stream, tokenizer, jsonStream, res)
} catch (error) {
console.error(error);
res.status(500).send('An error occurred');
}
}
And that's the whole magic! You can find the complete code in this repository.
I am big football (soccer) fan and follow sports news daily. However, even with this routine, I often find myself unfamiliar with the current standings in the Premier League, Serie A, or other leagues.
In my view, football coverage these days is overwhelmingly occupied with topics like transfer rumors, relegating actual match outcomes to secondary importance.
This observation sparked the idea for my latest project: Sports Dashboard. This Angular-based website displays the standings of the most prominent football leagues across Europe. Additionally, the Austrian Bundesliga features on the site not because it's one of Europe's most critical competitions, but because I reside in Austria and wish to stay informed about local football.
To provide the frontend with data I developed a new REST endpoint: GET https://abelcastro.dev/api/sports/standings/
.
The data will be automatically updated every day at 2 AM.
I have implemented a management command import_league_standings.py, which is scheduled to run daily via a cron job on the server.
My goal with this project was to become familiar with Angular, and for me, the best way to achieve that is by working on a real project. In this case, the project turned out to be something truly useful. I hope the Sports Dashboard will also help me to reduce the frequency of my visits to sports news pages.