Home
Backend from First Principles / Module 21 — Testing & Code Quality

Testing & Code Quality

The testing pyramid. Mocks vs fakes. SOLID, DRY, YAGNI — without dogma.


Testing Pyramid

The testing pyramid tells you where to invest testing effort:

Unit Tests (base — most of them)
• Test one function/class in isolation
• Mock dependencies (DB, cache, external APIs)
• Fast (milliseconds), cheap to write and run
• Test business logic, algorithms, utility functions

Integration Tests (middle)
• Test multiple components together
• Use real or in-memory database
• Test service + repository together
• Slower than unit tests

End-to-End Tests (top — fewest)
• Test the full stack from HTTP request to response
• Real database, real dependencies
• Slowest, most brittle, hardest to maintain
• Only for critical user flows


What to Test

Business logic — test every branch, every edge case, every rule.

Happy path: valid input → expected output.
Error cases: invalid input → correct error thrown.
Edge cases: empty arrays, zero values, max values, null inputs.

Test coverage: aim for high coverage on business logic (80-90%+). Coverage on boilerplate (handlers, mappings) matters less.

Don't test implementation details. Test behavior. If you refactor the internals without changing behavior, tests should still pass.

JavaScript
// Bad: testing implementation
expect(service.cache.set).toHaveBeenCalled();

// Good: testing behavior
const result = await service.getUser(id);
expect(result.email).toBe("alice@example.com");

Mocking & Dependency Injection

Services depend on repositories, which depend on databases. Testing the service shouldn't require a real database.

Solution: Dependency Injection + Mocking.

JavaScript
// Service accepts its dependencies (injectable)
class UserService {
  constructor(private userRepo: UserRepository, private mailer: Mailer) {}
}

// In tests, inject fakes
const mockRepo = {
  findById: jest.fn().mockResolvedValue({ id: "1", email: "alice@test.com" }),
  create: jest.fn().mockResolvedValue({ id: "2", email: "bob@test.com" }),
};
const mockMailer = { send: jest.fn().mockResolvedValue(undefined) };

const service = new UserService(mockRepo, mockMailer);

// Now test in isolation — no DB, no real emails
const user = await service.getUser("1");
expect(user.email).toBe("alice@test.com");

Code Quality Fundamentals

SOLID Principles (for classes/modules):
S — Single Responsibility: each class does one thing
O — Open/Closed: open for extension, closed for modification
L — Liskov Substitution: subclasses work in place of their parents
I — Interface Segregation: small, specific interfaces over large ones
D — Dependency Inversion: depend on abstractions, not concretions

DRY — Don't Repeat Yourself (but don't over-abstract prematurely)
YAGNI — You Ain't Gonna Need It (don't build what you don't need yet)
KISS — Keep It Simple, Stupid

Linting: ESLint (JS/TS), golangci-lint (Go), flake8/mypy (Python).
Formatting: Prettier (JS), gofmt (Go), black (Python).
PR reviews: catch logic errors, architecture smells, missing tests.


Source & Credit

The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.

⁂ Back to all modules