DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Testing Strategy for Solo Developers: What to Test, What to Skip, and Starting with Money Code

The Test You Skip Is the Bug in Production

Every developer knows they should write tests. Most developers skip them when they're in a hurry. The bugs that make it to production are always in the untested code.

Here's the testing strategy that's actually sustainable for a solo developer.

What to Test (The Priority Stack)

1. Money code (highest priority)
   Stripe webhook handlers
   Payment calculation logic
   Subscription access checks
   Invoice generation

2. Auth code
   Login/logout flows
   Permission checks
   Token validation
   Password reset flow

3. Core business logic
   Whatever your app's main feature does
   Edge cases in that feature

4. Everything else
   Low priority. Test when it's easy.
   Don't test getters/setters, UI rendering, config.
Enter fullscreen mode Exit fullscreen mode

Jest Setup for Next.js

npm install -D jest @types/jest jest-environment-jsdom ts-jest
npm install -D @testing-library/react @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode
// jest.config.js
const nextJest = require('next/jest')
const createJestConfig = nextJest({ dir: './' })

module.exports = createJestConfig({
  testEnvironment: 'node', // 'jsdom' for React component tests
  setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1'
  }
})
Enter fullscreen mode Exit fullscreen mode

Testing Stripe Webhook Logic

// __tests__/webhooks/stripe.test.ts
import { handleSubscriptionUpdated } from '@/lib/stripe-webhooks'
import { db } from '@/lib/db'

// Mock the database
jest.mock('@/lib/db', () => ({
  db: {
    subscription: {
      upsert: jest.fn(),
      update: jest.fn(),
    }
  }
}))

describe('handleSubscriptionUpdated', () => {
  it('upgrades user to pro when subscription becomes active', async () => {
    const mockSub = {
      id: 'sub_123',
      status: 'active',
      customer: 'cus_123',
      items: { data: [{ price: { nickname: 'pro', id: 'price_123' } }] },
      current_period_start: 1700000000,
      current_period_end: 1702592000,
      cancel_at_period_end: false,
      trial_end: null,
    }

    await handleSubscriptionUpdated(mockSub as any)

    expect(db.subscription.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        update: expect.objectContaining({ plan: 'pro', status: 'active' })
      })
    )
  })

  it('downgrades to free when subscription is canceled', async () => {
    const mockSub = {
      id: 'sub_123',
      status: 'canceled',
      customer: 'cus_123',
      items: { data: [{ price: { nickname: 'pro', id: 'price_123' } }] },
      current_period_start: 1700000000,
      current_period_end: 1702592000,
      cancel_at_period_end: false,
      trial_end: null,
    }

    await handleSubscriptionUpdated(mockSub as any)

    expect(db.subscription.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        update: expect.objectContaining({ plan: 'free', status: 'canceled' })
      })
    )
  })
})
Enter fullscreen mode Exit fullscreen mode

Testing Access Control

// __tests__/lib/access.test.ts
import { hasActiveSubscription } from '@/lib/access'

const mockDb = jest.fn()
jest.mock('@/lib/db', () => ({ db: { subscription: { findUnique: mockDb } } }))

describe('hasActiveSubscription', () => {
  it('returns true for active subscription', async () => {
    mockDb.mockResolvedValue({ status: 'active', currentPeriodEnd: new Date(Date.now() + 86400000) })
    expect(await hasActiveSubscription('user_1')).toBe(true)
  })

  it('returns false for canceled subscription', async () => {
    mockDb.mockResolvedValue({ status: 'canceled', cancelAtPeriodEnd: false })
    expect(await hasActiveSubscription('user_1')).toBe(false)
  })

  it('returns false for missing subscription', async () => {
    mockDb.mockResolvedValue(null)
    expect(await hasActiveSubscription('user_1')).toBe(false)
  })

  it('allows access during grace period for past_due', async () => {
    const threeDaysAgo = new Date(Date.now() - 3 * 86400000)
    mockDb.mockResolvedValue({ status: 'past_due', currentPeriodEnd: threeDaysAgo })
    expect(await hasActiveSubscription('user_1')).toBe(true) // 4 days left in grace
  })
})
Enter fullscreen mode Exit fullscreen mode

The Minimum Test Coverage Rule

Minimum required before shipping:
  - Stripe webhook handler: 100% branch coverage
  - Auth check function: 100% branch coverage
  - Core feature function: happy path + 2 edge cases

Everything else: test when you fix a bug
(the test prevents it from coming back)
Enter fullscreen mode Exit fullscreen mode

The AI SaaS Starter Kit Includes Tests

Stripe webhook handler tests, auth flow tests, and API route tests -- all pre-written so you ship with coverage from day one.

$99 one-time at whoffagents.com

Top comments (0)