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.
// 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.
// 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.
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.