Skip to content

Testing Strategy

Testing during modernization serves a different purpose than testing during greenfield development. In a new project, tests verify that code does what the developer intended. In a modernization project, tests verify that new code behaves identically to the old code — even when the developer does not fully understand every edge case in the legacy system.

This difference shapes everything: what to test, how much coverage to require, and what assertions to write.

Not all code requires the same coverage level. Grade components by their risk profile and test accordingly.

GradeCoverage TargetComponent TypesRationale
Critical95%+Financial calculations, authentication, data migrations, encryption, search/retrievalBugs here cause data loss, security breaches, or financial errors
Standard80%+Business logic, API endpoints, service layerBugs here affect functionality but are usually recoverable
Basic60%+Utilities, helpers, formatters, configurationBugs here are low-impact and easy to fix

During modernization, the migrated components should be graded at least one level higher than they would be in a stable codebase. A utility that formats dates is normally “Basic” — but if it is being migrated and the legacy system depends on its exact output format, it becomes “Standard” until parity is proven.

Different types of changes require different testing approaches.

Change TypeRequired TestsKey Assertion
New featureUnit + Integration + End-to-endFeature works as specified
Bug fixRegression test (must fail without fix, pass with fix)Bug cannot recur
RefactorParity test (old behavior = new behavior)No behavioral change
MigrationMigration + Rollback + Data integrityData survives round-trip
API changeContract + IntegrationConsumers are not broken
// A regression test must fail when the bug exists and pass after the fix.
// This proves the fix actually addresses the issue.
it('should handle zero-quantity line items without division error', () => {
// This input caused a division-by-zero in the legacy system
const lineItem = { quantity: 0, unitPrice: 100.00, discount: 0.10 };
const result = calculateLineTotal(lineItem);
expect(result).toEqual({ total: 0, tax: 0, discount: 0 });
});

The guiding principle: unless something is evidently verified and passing, it is failed. This means:

  • Tests fail by default when prerequisites are missing
  • No silent skipping of tests
  • No empty catch blocks that swallow errors
  • Environment variables control suite disabling (not ad-hoc .skip() calls)

Integration tests should verify their dependencies are available before running. When a dependency is missing, the test should fail with an actionable message — not silently pass.

it('should connect to the database', async () => {
try {
const result = await db.ping();
expect(result).toBeTruthy();
} catch {
// Database not running, skip silently
return; // This test "passes" even though nothing was verified
}
});

When a test suite genuinely cannot run in certain environments (CI without database access, for example), use explicit environment variables — not unconditional .skip().

const SKIP_INTEGRATION = process.env.SKIP_INTEGRATION === 'true';
describe('Integration Tests', () => {
if (SKIP_INTEGRATION) {
it.skip('skipped via SKIP_INTEGRATION=true', () => {});
return;
}
// Actual tests — they FAIL if dependencies are unavailable
it('should process a complete order workflow', async () => {
// ...
});
});

Vague assertions hide bugs. Specific assertions catch them.

Assertion TypeAnti-PatternBetter
Existenceexpect(result).toBeTruthy()expect(result).toEqual({ amount: 500, currency: 'USD' })
Countexpect(items.length).toBeGreaterThan(0)expect(items.length).toBe(4)
Invariantexpect(balance).toBeDefined()expect(balance.debit).toEqual(balance.credit)
Shapeexpect(typeof response).toBe('object')expect(response).toMatchObject({ status: 'approved', id: expect.any(String) })

For financial calculations, rounding, and currency operations, assert exact values:

// Not this:
expect(total).toBeCloseTo(100);
// This:
expect(total).toEqual(100.00);
expect(taxAmount).toEqual(18.00);
expect(grandTotal).toEqual(118.00);

A consistent directory structure helps teams find tests and understand what is covered.

src/
modules/
payment/
payment.service.ts
payment.service.test.ts # Unit tests (co-located)
test/
integration/
payment-workflow.test.ts # Cross-module integration
parity/
invoice-calculation.test.ts # Legacy vs. modern comparison
migration/
20260115-users.test.ts # Schema migration verification
regression/
BUG-1234-zero-qty.test.ts # Bug prevention (named by issue)
capabilities/
full-order-cycle.test.ts # End-to-end workflow
DirectoryPurposeWhen to Add
src/**/*.test.tsUnit tests, co-located with sourceEvery new module
test/integration/Cross-module workflowsWhen modules interact
test/parity/Legacy vs. modern comparisonEvery migrated component
test/migration/Schema and data migrationEvery database change
test/regression/Named by bug IDEvery bug fix
test/capabilities/End-to-end business flowsCritical user journeys

Vague Assertions

toBeTruthy(), toBeDefined(), toBeGreaterThan(0) — these pass when the value is wrong but present. Use exact values or structural matching.

Swallowed Errors

try/catch blocks that catch and ignore errors make tests pass when the system is broken. Assert on the error type and message instead.

Unconditional Skip

.skip() without an environment variable creates permanently dead tests. If a test cannot run, control it explicitly via configuration.

Tests That Pass When Services Are Down

A test that returns early when its dependency is unavailable is worse than no test — it creates false confidence that the integration works.

Parity tests are the signature testing pattern for modernization. They answer one question: does the new implementation produce the same output as the old one?

  1. Capture fixtures — Record real inputs and outputs from the legacy system
  2. Write parity tests — Feed the same inputs to the modern implementation
  3. Assert exact match — Output must be identical (or document accepted deviations)
  4. Run both sides — Keep legacy tests running until the modern system is proven

Accepted deviations (e.g., different date formatting, additional fields) must be documented and approved. Silent deviations are bugs.

MetricTargetMeaning
Parity coverage100% of migrated endpointsEvery migrated API has a parity test
Parity pass rate100% before cutoverZero behavioral differences
Fixture count3+ per endpoint (happy + edge + error)Adequate input diversity