DEV Community

Dakota Huang
Dakota Huang

Posted on

One Function, One Test, One Commit: The Smallest Safe Refactor

You have a messy repo. No tests. An AI suggests a sweeping refactor. You say no.

Big changes without a safety net are disasters. The safest unit is a single function. Lock it. Change it. Verify it. Commit it. Then repeat.

Here is a concrete workflow that uses a free model to write the lock and a free server to run it.

Step 1: Pick One Messy Function

Choose a function with clear inputs and outputs. Avoid functions with network calls or hidden state. A pure calculation is perfect.

// pricing.js — no tests, untouchable legacy
export function calculateTotal(cart, taxRate) {
  let subtotal = 0;
  for (const item of cart) {
    subtotal += item.price * item.qty;
  }
  let tax = subtotal * taxRate;
  if (cart.length > 5) {
    tax = tax * 0.9; // loyalty discount nobody remembers adding
  }
  return { subtotal, tax, total: subtotal + tax };
}
Enter fullscreen mode Exit fullscreen mode

Do not refactor yet. First, record what it does today.

Step 2: Generate a Characterization Test

Ask an AI to write a test that freezes current behavior. I used MonkeyCode's free model for this. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Give the model the function and a simple prompt:

Write a node:test test that calls calculateTotal with a cart of 2 items and taxRate 0.1. The test should only record the current output, not judge it.

The result:

import test from 'node:test';
import assert from 'node:assert/strict';
import { calculateTotal } from './pricing.js';

test('calculateTotal records current behavior', () => {
  const cart = [
    { price: 10, qty: 2 },
    { price: 5, qty: 1 },
  ];
  const result = calculateTotal(cart, 0.1);
  assert.deepEqual(result, { subtotal: 25, tax: 2.5, total: 27.5 });
});
Enter fullscreen mode Exit fullscreen mode

Read that test carefully. Free models can invent outputs. If the expected value looks wrong, run the function with known numbers and confirm before trusting it.

Step 3: Run the Lock on a Free Server

You need an environment where the test runs in isolation. Configure a checkout, install dependencies, and run the suite.

MonkeyCode's free server option gives you a disposable environment for this. No CI setup. No shared runner. Just a clean slate.

npm test
Enter fullscreen mode Exit fullscreen mode

The test passes. Now you have a baseline.

Step 4: Make the Smallest Change

The goal is one commit that does not change behavior. Extract the tax logic into a helper function. Do not touch anything else.

export function calculateTotal(cart, taxRate) {
  const subtotal = calcSubtotal(cart);
  const tax = calcTax(subtotal, cart.length, taxRate);
  return { subtotal, tax, total: subtotal + tax };
}

function calcSubtotal(cart) {
  return cart.reduce((acc, item) => acc + item.price * item.qty, 0);
}

function calcTax(subtotal, itemCount, taxRate) {
  let tax = subtotal * taxRate;
  if (itemCount > 5) {
    tax *= 0.9;
  }
  return tax;
}
Enter fullscreen mode Exit fullscreen mode

Run the same test again.

It still passes. The refactor did not change behavior.

Step 5: Commit and Repeat

Commit the test and the refactor together. One function. One test. One commit.

git add pricing.js pricing.test.js
git commit -m "Extract tax helpers from calculateTotal"
Enter fullscreen mode Exit fullscreen mode

Then pick the next function. Repeat the cycle.

Why This Works

The scope is tiny. A failure in a one-function change is easy to locate. The characterization test catches regressions immediately. The free tools remove friction: no local VM, no paid model credits.

This is not a rewrite strategy. It is a patience strategy. You turn a rat's nest into a sequence of verified microsteps.

Limitations

Free models have limits. They may generate tests that match a hallucinated version of the code. Always compare test output to actual runtime behavior.

Free server options also have resource limits. Long-running suites or heavy parallel tests may not fit. Keep the work small.

Characterization tests record current behavior. They do not validate that behavior. If the logic is buggy, your lock freezes the bug.

Who Should Not Use This

Do not use this workflow if you need a full architectural rewrite in one sprint. It will feel too slow.

Do not use it if your repo has no clear function boundaries. You first need to identify the functions.

And if your code cannot be copied to an external environment for testing, verify where the free server runs before touching it.

Start With One Function

Open the messiest file. Find the function that scares you most. Write one characterization test. Run it on the free server. Make the smallest change. Commit.

That is the entire method. Small steps are boring. They are also the only refactors that ship safely.

Top comments (0)