Vague Assertions
toBeTruthy(), toBeDefined(), toBeGreaterThan(0) — these pass when the value is wrong but present. Use exact values or structural matching.
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.
| Grade | Coverage Target | Component Types | Rationale |
|---|---|---|---|
| Critical | 95%+ | Financial calculations, authentication, data migrations, encryption, search/retrieval | Bugs here cause data loss, security breaches, or financial errors |
| Standard | 80%+ | Business logic, API endpoints, service layer | Bugs here affect functionality but are usually recoverable |
| Basic | 60%+ | Utilities, helpers, formatters, configuration | Bugs 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 Type | Required Tests | Key Assertion |
|---|---|---|
| New feature | Unit + Integration + End-to-end | Feature works as specified |
| Bug fix | Regression test (must fail without fix, pass with fix) | Bug cannot recur |
| Refactor | Parity test (old behavior = new behavior) | No behavioral change |
| Migration | Migration + Rollback + Data integrity | Data survives round-trip |
| API change | Contract + Integration | Consumers 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 });});// A parity test runs the same input through legacy and modern implementations// and asserts identical output.
it('should produce identical invoice totals as legacy calculator', () => { const invoice = loadFixture('invoice-with-mixed-tax-rates');
const legacyResult = legacyCalculator.computeTotal(invoice); const modernResult = modernCalculator.computeTotal(invoice);
expect(modernResult.total).toEqual(legacyResult.total); expect(modernResult.taxBreakdown).toEqual(legacyResult.taxBreakdown); expect(modernResult.lineItems).toEqual(legacyResult.lineItems);});// A migration test verifies data integrity through schema changes.
it('should migrate user records without data loss', async () => { // Seed with known data await seedDatabase(migrationFixtures.users);
// Run migration forward await runMigration('20260115_restructure_users'); const afterMigrate = await queryAllUsers();
// Run migration backward await runMigration('20260115_restructure_users', { direction: 'down' }); const afterRollback = await queryAllUsers();
// Verify round-trip preserves data expect(afterRollback).toEqual(migrationFixtures.users); // Verify forward migration has expected shape expect(afterMigrate[0]).toHaveProperty('profile.displayName');});The guiding principle: unless something is evidently verified and passing, it is failed. This means:
.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 }});beforeAll(async () => { const health = await checkDependencies({ database: 'postgres://localhost:5432', cache: 'redis://localhost:6379', });
const missing = Object.entries(health) .filter(([_, ok]) => !ok) .map(([name]) => name);
if (missing.length > 0) { throw new Error( `Required services unavailable: ${missing.join(', ')}\n\n` + 'To fix:\n' + ' 1. Run: docker compose up -d\n' + ' 2. Wait for health checks: docker compose ps\n' + ' 3. Or disable: SKIP_INTEGRATION=true to skip this suite\n' ); }});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 Type | Anti-Pattern | Better |
|---|---|---|
| Existence | expect(result).toBeTruthy() | expect(result).toEqual({ amount: 500, currency: 'USD' }) |
| Count | expect(items.length).toBeGreaterThan(0) | expect(items.length).toBe(4) |
| Invariant | expect(balance).toBeDefined() | expect(balance.debit).toEqual(balance.credit) |
| Shape | expect(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| Directory | Purpose | When to Add |
|---|---|---|
src/**/*.test.ts | Unit tests, co-located with source | Every new module |
test/integration/ | Cross-module workflows | When modules interact |
test/parity/ | Legacy vs. modern comparison | Every migrated component |
test/migration/ | Schema and data migration | Every database change |
test/regression/ | Named by bug ID | Every bug fix |
test/capabilities/ | End-to-end business flows | Critical 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?
Accepted deviations (e.g., different date formatting, additional fields) must be documented and approved. Silent deviations are bugs.
| Metric | Target | Meaning |
|---|---|---|
| Parity coverage | 100% of migrated endpoints | Every migrated API has a parity test |
| Parity pass rate | 100% before cutover | Zero behavioral differences |
| Fixture count | 3+ per endpoint (happy + edge + error) | Adequate input diversity |