Unit Testing for Developers Who've Never Written a Test
Most self-taught developers have never written a test. Here's why testing matters, how to start without the overhead, and the testing mindset that makes you a better developer.
Why You Probably Haven't Written Tests (And Why That Has to Change)
The truth about self-taught developers and testing: almost none of them write tests when they're learning. The reason is intuitive — testing adds overhead to building, and when you're learning to build, you want to build, not test. This is a rational short-term tradeoff. The long-term cost is that you enter the professional world without one of the most fundamental engineering practices. Companies don't just want features — they want features that don't break existing features. That guarantee requires tests.
What Tests Actually Are (Demystified)
A unit test is code that calls your code with specific inputs and asserts the output matches expectations. That's it. No magic, no framework complexity you can't understand. The framework (Jest, Vitest, pytest) provides utilities for running tests and reporting results. The test itself is just: call function with X, expect result to equal Y.
// Your first unit test:
// src/utils/formatPrice.ts
export function formatPrice(cents: number): string {
return `$${(cents / 100).toFixed(2)}`
}
// src/utils/formatPrice.test.ts
import { formatPrice } from './formatPrice'
describe('formatPrice', () => {
it('formats cents to dollar string', () => {
expect(formatPrice(1000)).toBe('$10.00')
})
it('handles zero', () => {
expect(formatPrice(0)).toBe('$0.00')
})
it('handles odd cents', () => {
expect(formatPrice(1099)).toBe('$10.99')
})
})
// Run with: npx jest formatPrice
// Green = passes. Red = fails with a useful message.What to Test (And What Not To)
Test your logic, not the framework. Specifically: test every function with complex branching logic, test edge cases (empty arrays, null values, maximum values), test error conditions (what happens when the database is down?), test functions with side effects (does it call the email service?). Don't test: trivial getters and setters, third-party library functionality, implementation details that will change. The 80/20 rule for testing: test the behavior users care about, not the internal implementation. A test that breaks when you rename a variable is a bad test.
Test-Driven Development: The Controversial Practice
Test-driven development (TDD) says write the test first, then write the code to make it pass. Many developers find this feels backwards. Many others swear by it. The truth: TDD is a tool, not a religion. It's particularly useful when: the requirements are clear and testable upfront, you're building a function with complex business logic, you're debugging a tricky edge case (write a test that reproduces the bug first). It's less useful when: you're exploring an unfamiliar API, you're prototyping UI, you're uncertain about the design. Try TDD on your next utility function. Form your own opinion.
// TDD example: building a discount calculator
// Step 1: Write the failing test
it('applies 10% discount to orders over $100', () => {
expect(calculateDiscount(120_00)).toBe(10_80) // $12.00 discount on $120
})
// Step 2: Write minimal code to make it pass
function calculateDiscount(amountCents: number): number {
if (amountCents > 100_00) return Math.round(amountCents * 0.10)
return 0
}
// Step 3: Refactor, keeping the test passing
// Step 4: Add tests for more cases, repeat
it('applies no discount to orders under $100', () => {
expect(calculateDiscount(99_99)).toBe(0)
})
it('applies 20% discount to orders over $500', () => {
expect(calculateDiscount(500_00)).toBe(100_00)
})Starting a Testing Practice: The Minimal Viable Approach
Don't try to achieve 100% test coverage immediately — you'll get overwhelmed and quit. Instead: install Jest (or Vitest for Vite projects), write one test for the most important function in your codebase, run it in your CI pipeline so it runs automatically, add tests whenever you fix a bug (write a test that reproduces the bug before fixing it). After one month of this practice, you'll have a meaningful test suite for the parts of your app that have historically been most fragile. The testing module at Beyond Vibe Code covers Jest, integration testing, and when to use each, with exercises building test suites for real projects.