Skip to content

Commit 1a02bad

Browse files
feat: update README.md
1 parent cb413ab commit 1a02bad

File tree

7 files changed

+104
-27
lines changed

7 files changed

+104
-27
lines changed

README.md

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This repository contains a small backend server built with **Bun**, **Hono**, **Redis**, and **TypeScript**, designed as a playground for exploring different testing strategies.
44

5-
The goal of this project is to provide clear, practical examples of **unit tests**, **integration tests**, and **end-to-end (E2E) tests** while experimenting with modern backend tools.
5+
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.
66

77
## `🚀 Tech Stack`
88

@@ -18,7 +18,84 @@ This project is intended as a **testing playground**, where you can:
1818
- Write and run **unit tests** for isolated logic
1919
- Build **integration tests** that interact with Redis and other services
2020
- Run **end-to-end tests** that exercise the entire API
21-
- Experiment with backend techniques using Bun and Hono
21+
- Experiment with backend design patterns like singleton services
22+
- Learn modern testing patterns in a minimal, modular codebase
23+
24+
## `🔹 Singleton services`
25+
26+
Both RedisService and HttpServer are implemented as singletons:
27+
28+
- Ensures only one instance of Redis or HTTP server exists
29+
- Makes it easier to test, since you can start/stop them globally
30+
- Avoids conflicts when running multiple tests or endpoints simultaneously
31+
32+
## `🧪 How tests work in this project`
33+
34+
### `Unit tests`
35+
36+
- Test individual services or functions in isolation, without starting HTTP server or connecting to Redis.
37+
- Focused on logic correctness.
38+
Example: Testing `RedisService.lpush` with a mocked Redis client.
39+
40+
```ts
41+
test("should call lPush on the client", async () => {
42+
const fakeClient = {
43+
lPush: mock(async () => 1),
44+
};
45+
46+
(RedisService as any).client = fakeClient;
47+
48+
await RedisService.lpush("messages", "hello");
49+
50+
expect(fakeClient.lPush).toHaveBeenCalled();
51+
expect(fakeClient.lPush).toHaveBeenCalledWith("messages", "hello");
52+
});
53+
```
54+
55+
### `Integration tests`
56+
57+
- Test how services work together.
58+
- RedisService singleton is used as-is, no mocking.
59+
- Redis must be running (`docker compose up -d`).
60+
61+
```ts
62+
test("POST /messages → stores a new message successfully", async () => {
63+
const req = new Request("http://localhost/messages", {
64+
method: "POST",
65+
body: JSON.stringify({ text: "hi" }),
66+
});
67+
68+
const res = await messageRoutes.request("/", req);
69+
expect(res.status).toBe(200);
70+
});
71+
```
72+
73+
### `End-to-End (E2E) tests`
74+
75+
- Test the full flow, including HTTP routes and Redis.
76+
- Simulate real API usage: POST a message → GET messages.
77+
- Ensures everything works together.
78+
79+
```ts
80+
test("Should save a valid message and then retrieve it", async () => {
81+
const body = {
82+
text: "hi",
83+
};
84+
const postResponse = await axios.post<PostMessageResponse>(
85+
"http://localhost:3000/messages",
86+
body,
87+
);
88+
expect(postResponse.status).toBe(200);
89+
expect(postResponse.data.ok).toBeTruthy();
90+
91+
const getResponse = await axios.get<GetMessagesResponse>(
92+
"http://localhost:3000/messages",
93+
);
94+
expect(getResponse.status).toBe(200);
95+
expect(getResponse.data.count).toEqual(1);
96+
expect(getResponse.data.messages.length).toEqual(1);
97+
});
98+
```
2299

23100
## `▶️ Running the server`
24101

@@ -34,11 +111,13 @@ The server will automatically reload when files change.
34111
bun test
35112
```
36113

37-
Tests will be progressively added across all categories (unit, integration, and E2E).
114+
- **Unit tests**: Test logic in isolation
115+
- **Integration tests**: Test services and external dependencies (Redis)
116+
- **E2E tests**: Test full API behavior
38117

39118
---
40119

41120
### `📝 Notes`
42121

43-
This project is intentionally minimal and modular to make testing patterns easy to understand and extend.
44-
More test cases, utilities, and scenarios will be added over time.
122+
- The project is intentionally minimal and modular to make testing patterns easy to understand.
123+
- The README aims to provide immediate understanding of unit vs integration vs E2E tests in a practical backend project.

src/services/redisService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ export class RedisService {
44
private static client: ReturnType<typeof createClient> | null = null;
55

66
static async start() {
7-
const c = createClient({ url: "redis://localhost:6379" });
8-
await c.connect();
9-
this.client = c;
7+
const rc = createClient({ url: "redis://localhost:6379" });
8+
await rc.connect();
9+
this.client = rc;
1010
console.log("\x1b[32m 💾 Redis connected successfully \x1b[0m");
1111
}
1212

src/types/redisTypes.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

test/example.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, test, expect } from "bun:test";
22

3-
describe("Example test case", () => {
4-
test("Simple sum", async () => {
5-
const sum = 2 + 2;
6-
expect(sum).toBe(4);
3+
describe("Math utilities – basic operations", () => {
4+
test("should correctly sum two positive integers", () => {
5+
const a = 2;
6+
const b = 2;
7+
const result = a + b;
8+
expect(result).toBe(4);
79
});
810
});

test/integration/messageRoutes.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, afterAll, beforeAll } from "bun:test";
1+
import { describe, test, expect, afterAll, beforeAll } from "bun:test";
22
import { messageRoutes } from "../../src/routes/messages";
33
import { RedisService } from "../../src/services/redisService.ts";
44
import type { ErrorResponse, GetMessagesResponse } from "../utils/types.ts";
@@ -14,7 +14,7 @@ describe("Message Routes – Integration Tests", () => {
1414
});
1515

1616
describe("Success cases", () => {
17-
it("GET /messages → returns an empty list when no messages exist", async () => {
17+
test("GET /messages → returns an empty list when no messages exist", async () => {
1818
const req = new Request("http://localhost/messages");
1919
const res = await messageRoutes.request("/", req);
2020

@@ -23,7 +23,7 @@ describe("Message Routes – Integration Tests", () => {
2323
expect(data.count).toBe(0);
2424
});
2525

26-
it("POST /messages → stores a new message successfully", async () => {
26+
test("POST /messages → stores a new message successfully", async () => {
2727
const req = new Request("http://localhost/messages", {
2828
method: "POST",
2929
body: JSON.stringify({ text: "hi" }),
@@ -33,7 +33,7 @@ describe("Message Routes – Integration Tests", () => {
3333
expect(res.status).toBe(200);
3434
});
3535

36-
it("GET /messages → returns the previously stored message", async () => {
36+
test("GET /messages → returns the previously stored message", async () => {
3737
const req = new Request("http://localhost/messages");
3838
const res = await messageRoutes.request("/", req);
3939

@@ -42,7 +42,7 @@ describe("Message Routes – Integration Tests", () => {
4242
expect(data.count).toBe(1);
4343
});
4444

45-
it("POST /messages → returns only the first 10 messages after inserting 15", async () => {
45+
test("POST /messages → returns only the first 10 messages after inserting 15", async () => {
4646
for (let i = 0; i < 15; i++) {
4747
const req = new Request("http://localhost/messages", {
4848
method: "POST",
@@ -68,7 +68,7 @@ describe("Message Routes – Integration Tests", () => {
6868
});
6969

7070
describe("Error cases", () => {
71-
it("POST /messages → returns 400 when body is missing", async () => {
71+
test("POST /messages → returns 400 when body is missing", async () => {
7272
const req = new Request("http://localhost/messages", {
7373
method: "POST",
7474
});
@@ -80,7 +80,7 @@ describe("Message Routes – Integration Tests", () => {
8080
expect(data.error).toEqual("Invalid body: expected { text: string }");
8181
});
8282

83-
it("POST /messages → returns 400 when text is not a string", async () => {
83+
test("POST /messages → returns 400 when text is not a string", async () => {
8484
const req = new Request("http://localhost/messages", {
8585
method: "POST",
8686
body: JSON.stringify({ text: 10 }),

test/unit/redisService.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { describe, it, expect, mock, afterEach } from "bun:test";
1+
import { describe, test, expect, mock, afterEach } from "bun:test";
22
import { RedisService } from "../../src/services/redisService";
33

44
describe("RedisService - Unit tests", () => {
55
afterEach(() => {
66
(RedisService as any).client = null;
77
});
88

9-
it("should call lPush on the client", async () => {
9+
test("should call lPush on the client", async () => {
1010
const fakeClient = {
1111
lPush: mock(async () => 1),
1212
};
@@ -19,7 +19,7 @@ describe("RedisService - Unit tests", () => {
1919
expect(fakeClient.lPush).toHaveBeenCalledWith("messages", "hello");
2020
});
2121

22-
it("should call lRange on the client", async () => {
22+
test("should call lRange on the client", async () => {
2323
const fakeClient = {
2424
lRange: mock(async () => ["hi"]),
2525
};
@@ -33,7 +33,7 @@ describe("RedisService - Unit tests", () => {
3333
expect(result).toEqual(["hi"]);
3434
});
3535

36-
it("should return 15 elementos from 0 to 14", async () => {
36+
test("should return 15 items from 0 to 14", async () => {
3737
const fakeClient = {
3838
lRange: mock(async () =>
3939
Array.from({ length: 15 }, (_, i) => `msg-${i}`),

0 commit comments

Comments
 (0)