The Trade-off: Clean Testing vs. Code Brevity in Modern JS
Source: Dev.to
Hey fellow devs! š
In modern JavaScript and TypeScript development we constantly balance two opposing forces:
- Code Brevity ā writing concise, minimal code.
- Clean Testing ā writing code that is easy to isolate and verify.
Often the code that is fastest to write is the hardest to test. Conversely, code designed for testability often looks āboilerplateāheavyā at first glance.
Below we explore this tradeāoff through realāworld examples, moving from common patterns to the mindset required for architecting longāterm, scalable systems.
RoundāÆ1: The Environment Variable Dilemma
A classic debate that shows up in code reviews: how do you access environment variables (API keys, feature flags, etc.) provided by Vite, Webpack, or Node?
The āBriefā Approach (Static Constants)
The fastest way is to read the variable directly and store it in a constant. One line of code, simple.
// config.ts
export const IS_PRODUCTION = import.meta.env.PROD;
export const API_URL = import.meta.env.VITE_API_URL;
// myFeature.ts
import { IS_PRODUCTION } from './config';
if (IS_PRODUCTION) {
// do scary real things
}
The Hidden Cost
- The code is tightly coupled to the build systemās global state.
- When you unitātest
myFeature.ts,IS_PRODUCTIONis evaluated immediately when the test file loads. - Once the constant is set to
trueorfalse, it is extremely difficult to change it within the same test run.
To test both scenarios you often resort to āstubbing globallyā, e.g. telling Vitest or Jest to alter the runtime environment:
// ā Messy global testing
vi.stubEnv('PROD', 'true'); // now EVERY test thinks itās prod
// ā¦if you forget to unstub, other tests break mysteriously
The āTestableā Approach (Getter Functions)
Wrap the access in a function. It adds a tiny bit of boilerplate but creates a clean seam.
// config.ts
export const getIsProduction = () => import.meta.env.PROD;
// myFeature.ts
import { getIsProduction } from './config';
if (getIsProduction()) {
// do scary real things
}
The Benefit: Creating a Seam
A Seam (popularized by Michael Feathers) is a place where you can alter program behavior without editing the source code. In our tests we no longer need to hack the global environment; we just spy on a regular function.
// ā
Clean isolated testing
import * as Config from './config';
test('does scary things only in prod', () => {
const spy = vi.spyOn(Config, 'getIsProduction');
spy.mockReturnValue(true);
// run expectations for prod...
spy.mockReturnValue(false);
// run expectations for nonāprod...
});
The testable approach trades a few extra characters for isolation and control.
RoundāÆ2: Dealing with Time
Another area where brevity hurts testing is handling the current time.
The āBriefā Approach (Direct Access)
// discount.ts
export const isDiscountExpired = (expiryDate: Date): boolean => {
// Brevity wins here:
const now = new Date();
return now > expiryDate;
};
The problem: the function is nonādeterministic. A test that passes today may fail tomorrow. To test it you usually need heavyāhanded āfake timersā that freeze the system clock.
The āTestableā Approach (Dependency Injection)
Inject the time source via a defaulted parameterāa lightweight form of Dependency Injection.
// discount.ts
export const isDiscountExpired = (
expiryDate: Date,
now: Date = new Date() // default keeps app code simple
): boolean => {
return now > expiryDate;
};
Now the test is trivial and deterministicāno need to mock the system clock.
// ā
Clean testing
test('checks expiration', () => {
const fixedNow = new Date('2024-01-01T10:00:00Z');
const tomorrow = new Date('2024-01-02T10:00:00Z');
expect(isDiscountExpired(tomorrow, fixedNow)).toBe(false);
});
The Senior Engineerās Mindset
Junior / midālevel developers often measure success by velocityāhow fast a feature ships. Brevity boosts shortāterm speed.
Senior / principal engineers shift focus to maintainability, stability, and risk reduction.
ShiftāLeft
We want to āshift leftā on bugs: catch them early in unit tests on a developerās machine rather than later in QA or production.
If code is brief but relies on global state (import.meta.env, new Date(), etc.), developers instinctively avoid writing tests because theyāre painful to set up. Introducing seams (getter functions, injectable dependencies) makes testing easy, encouraging a healthier testing culture.
By introducing slight amounts of boilerplate, creating getter functions, injecting dependencies, and creating seams, we lower the friction required to write a test.
Conclusion
Choose Brevity for throwaway prototypes, simple scripts, or incredibly confined UI components that have zero logic.
Choose Testability for business logic, configuration, helpers, and anything that your application relies on to function correctly over time.
It looks like more code today, but it buys you peace of mind tomorrow.
If this helped, give it a heart! ā¤ļø
#Hash