This repository contains a small backend server built with Bun, Hono, Redis, and TypeScript, designed as a playground for exploring different testing strategies.
The project demonstrates how to structure a backend with singleton services and how to write unit, integration, and end-to-end (E2E) tests using real-world tools.
- Bun β Ultra-fast JavaScript runtime used to run the server and tests
- Hono β Lightweight and high-performance web framework
- Redis β In-memory data store used for caching and message handling
- TypeScript β Static typing to improve maintainability and code quality
This project is intended as a testing playground, where you can:
- Write and run unit tests for isolated logic
- Build integration tests that interact with Redis and other services
- Run end-to-end tests that exercise the entire API
- Experiment with backend design patterns like singleton services
- Learn modern testing patterns in a minimal, modular codebase
Both RedisService and HttpServer are implemented as singletons:
- Ensures only one instance of Redis or HTTP server exists
- Makes it easier to test, since you can start/stop them globally
- Avoids conflicts when running multiple tests or endpoints simultaneously
- Test individual services or functions in isolation, without starting HTTP server or connecting to Redis.
- Focused on logic correctness.
Example: Testing
RedisService.lpushwith a mocked Redis client.
test("Should call lPush on the client", async () => {
const fakeClient = {
lPush: mock(async () => 1),
};
(RedisService as any).client = fakeClient;
await RedisService.lpush("messages", "hello");
expect(fakeClient.lPush).toHaveBeenCalled();
expect(fakeClient.lPush).toHaveBeenCalledWith("messages", "hello");
});- Test how services work together.
- RedisService singleton is used as-is, no mocking.
- Redis must be running (
docker compose up -d).
test("POST /messages β stores a new message successfully", async () => {
const req = new Request("http://localhost/messages", {
method: "POST",
body: JSON.stringify({ text: "hi" }),
});
const res = await messageRoutes.request("/", req);
expect(res.status).toBe(200);
});- Test the full flow, including HTTP routes and Redis.
- Simulate real API usage: POST a message β GET messages.
- Ensures everything works together.
test("Should save a valid message and then retrieve it", async () => {
const body = {
text: "hi",
};
const postResponse = await axios.post<PostMessageResponse>(
"http://localhost:3000/messages",
body,
);
expect(postResponse.status).toBe(200);
expect(postResponse.data.ok).toBeTruthy();
const getResponse = await axios.get<GetMessagesResponse>(
"http://localhost:3000/messages",
);
expect(getResponse.status).toBe(200);
expect(getResponse.data.count).toEqual(1);
expect(getResponse.data.messages.length).toEqual(1);
});Here is a list of all available commands defined in the package.json, along with what each one does:
| Command | Description |
|---|---|
bun run dev |
Runs the server in development mode with hot-reload enabled. |
bun start |
Starts the server normally (without hot-reload). |
bun test |
Runs all tests (unit, integration, and E2E). |
bun run build |
Builds the project into the dist/ folder, minified and optimized for Bun. |
bun run format |
Formats all files in the project using Prettier. |
bun coverage |
Runs the test suite and generates a coverage report. |
- The project is intentionally minimal and modular to make testing patterns easy to understand.
- The README aims to provide immediate understanding of unit vs integration vs E2E tests in a practical backend project.