Test SRP compliance via behavioral and structural indicators. Behavioral tests: (1) Count test file responsibilities - if testing UserService requires mocking database, email, validation, caching, logging = SRP violation (5 responsibilities). SRP compliant: Only mock user repository. (2) Change impact analysis - modify feature, count test files needing updates. >5 files = scattered responsibility. (3) Test setup complexity - if beforeEach() has 20 lines of mocks = too many dependencies. Structural tests: (1) Static analysis - class >500 lines, >20 methods flags SRP risk. (2) Import analysis - class importing from 5+ different domains (db, email, http, cache, crypto) = multiple responsibilities. (3) Method cohesion - LCOM >0.8 indicates low cohesion. Test pattern: describe('UserService', () => {const mockRepo = {save: jest.fn(), find: jest.fn()}; const service = new UserService(mockRepo); test('creates user', async () => {await service.create(userData); expect(mockRepo.save).toHaveBeenCalled();}); }). If test requires 10 mocks, extract responsibilities. Metrics: Cyclomatic complexity per method (<10), class size (<300 lines), number of dependencies (<5). Tools: SonarQube, CodeClimate flag SRP violations. TDD approach: Write test first - if hard to test, likely violates SRP.
Solid Testing Strategies FAQ & Answers
30 expert Solid Testing Strategies answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
30 questionsVerify OCP via extension tests - add new behavior without modifying existing code or tests. Test pattern: Base tests remain unchanged when adding variants. Example: describe('PaymentProcessor', () => {testProcessor(new CreditCardProcessor()); testProcessor(new PayPalProcessor()); testProcessor(new BitcoinProcessor());}); function testProcessor(processor: PaymentProcessor) {test('processes valid payment', () => {expect(processor.process(100)).resolves.toBe(true);}); test('rejects invalid amount', () => {expect(processor.process(-1)).rejects.toThrow();});}}. OCP compliance: Adding BitcoinProcessor doesn't modify testProcessor() function or existing tests. Violations to detect: (1) Adding variant requires modifying existing test suite. (2) Tests with if(processor.type === 'credit') conditionals. (3) Tight coupling - tests know concrete implementations. Refactoring test indicator: Count modified lines when adding feature. OCP compliant: Add new test file for variant, zero lines changed in existing tests. Metrics: Test churn - files modified per feature. High churn = OCP violation. Modern: Parameterized tests (Jest describe.each, Vitest test.each) enable testing all variants with single test definition. Coverage: Each implementation has own test file + shared interface tests. Goal: Add BitcoinProcessor.test.ts, existing CreditCard.test.ts unchanged.
LSP testing: Write tests against base class, verify all subclasses pass identical tests without modification. Pattern: describe('Storage interface', () => {testStorage(new LocalStorage()); testStorage(new S3Storage()); testStorage(new MemoryStorage());}); function testStorage(storage: IStorage) {test('saves and retrieves', async () => {await storage.save('key', 'value'); const result = await storage.get('key'); expect(result).toBe('value');}); test('returns null for missing key', async () => {expect(await storage.get('nonexistent')).toBeNull();}); test('overwrites existing key', async () => {await storage.save('key', 'v1'); await storage.save('key', 'v2'); expect(await storage.get('key')).toBe('v2');});}}. LSP compliance: All storage implementations pass testStorage() without special cases. Violations: (1) if(storage instanceof S3Storage) skip test - S3Storage doesn't honor contract. (2) Different error types - S3 throws AwsError, Local throws FileError. (3) Different return types - S3 returns Buffer, Local returns string. Refactoring: Fix implementations to match contract. Benefits: Substitutability verified, behavioral compatibility guaranteed, safe polymorphism. Modern: Property-based testing (fast-check) generates random inputs, tests all implementations with same property. Coverage: 100% of base interface methods tested with all implementations.
Test ISP via mock analysis - count mocked methods vs used methods. Pattern: test('OrderService processes order', () => {const mockPayment = {process: jest.fn().mockResolvedValue(true), refund: jest.fn(), generateReport: jest.fn(), auditLog: jest.fn(), updateSettings: jest.fn()}; const service = new OrderService(mockPayment); service.processOrder(order); expect(mockPayment.process).toHaveBeenCalled(); // Only process() used, 4 methods stubbed unnecessarily}). ISP violation: OrderService depends on PaymentProcessor interface with 5 methods but uses only 1. Refactoring: Extract focused interface. interface PaymentExecutor {process(amount): Promise
Test DIP via dependency injection and test doubles - if can't inject mocks, violates DIP. Pattern: class OrderService {constructor(private db: IDatabase, private email: IEmailSender) {}}. Test: const mockDb: IDatabase = {save: jest.fn(), find: jest.fn()}; const mockEmail: IEmailSender = {send: jest.fn()}; const service = new OrderService(mockDb, mockEmail); await service.processOrder(order); expect(mockDb.save).toHaveBeenCalledWith(order); expect(mockEmail.send).toHaveBeenCalled(). DIP compliance: OrderService testable with mocks, no real database/email needed. Violations: (1) Can't test without real dependencies - class OrderService {private db = new MySQLDatabase()} forces integration test. (2) Hard to mock - Service Locator pattern requires global setup. (3) Concrete dependencies - new CreditCardProcessor() in code, can't swap. Test doubles: (1) Mock - verify interactions (expect(db.save).toHaveBeenCalled()). (2) Stub - provide canned responses (jest.fn().mockResolvedValue(user)). (3) Fake - working implementation (InMemoryDatabase for tests). (4) Spy - wrap real object to verify calls. Modern: Jest/Vitest automatic mocks, TypeScript jest.Mocked
Contract tests verify interface contracts are honored by all implementations - ensures LSP and ISP compliance. Pattern: interface IPaymentGateway {charge(amount: number): Promise
Mutation testing verifies test quality by introducing bugs (mutants) - helps detect untested SOLID violations. Process: (1) Tool mutates code (change > to >=, remove if condition, swap return values). (2) Run test suite against mutant. (3) If tests pass = escaped mutant = weak tests. Goal: Kill all mutants (tests fail for mutations). SOLID connection: Well-designed code (SOLID) easier to mutation test. SRP: Small focused classes have focused tests, easier to kill mutants. Example: class Validator {validate(email: string) {return /\S+@\S+/.test(email);}}. Mutation: return true (remove regex). Test: expect(validator.validate('invalid')).toBe(false) kills mutant. OCP: Mutation tests verify all implementations. Mutate StripeProcessor.charge(), tests for PayPal should pass (isolated), tests for Stripe should fail (specific). Tools: Stryker (JavaScript/TypeScript), PITest (Java). Configuration: stryker init, stryker run. Metrics: Mutation score = killed mutants / total mutants. Target: >80%. Benefits: Finds weak tests (assertions missing), verifies code actually tested (not just covered), improves test quality. Cost: Slow (runs tests N times for N mutants). Use on critical paths. Modern: Differential mutation testing (mutate only changed code), parallel execution (faster).
TDD enforces SRP by making design pain obvious during test writing - hard to test = SRP violation. Red-Green-Refactor with SRP: (1) Red - Write test for single responsibility. test('UserService creates user', () => {const service = new UserService(mockRepo); service.create(user); expect(mockRepo.save).toHaveBeenCalled();}). (2) Green - Implement minimum code to pass. class UserService {constructor(private repo: UserRepository) {} create(user: User) {this.repo.save(user);}}. (3) Refactor - Extract additional responsibilities. If service.create() also validates, sends email, logs - extract to separate classes. TDD signals for SRP violations: (1) Test setup is complex (10+ mocks needed). (2) Testing one method requires testing unrelated concerns. (3) Mockito/Jest mock hell (mocking mocks). (4) Can't name test clearly ('creates user and sends email and logs' = multiple responsibilities). Refactoring: test('UserService creates user', ...); test('EmailService sends welcome email', ...); test('Logger logs user creation', ...). Benefits: SRP violations caught at test-writing time (before implementation), forces thinking about responsibilities upfront, results in testable code. Modern: Outside-in TDD (start with acceptance test, drill down to unit tests) naturally creates focused responsibilities. Coverage: One test file per responsibility.
Characterization tests document existing behavior before refactoring - safety net for SOLID improvements to legacy code. Process: (1) Write tests describing current behavior (even if buggy). (2) Run tests, verify they pass. (3) Refactor towards SOLID. (4) Tests ensure behavior unchanged. Pattern: test('legacy UserService behavior', () => {const service = new UserService(); const result = service.processUser({name: 'Alice', email: '[email protected]'}); expect(result).toEqual({id: expect.any(Number), name: 'ALICE', email: '[email protected]', createdAt: expect.any(Date), welcomeEmailSent: true});}). Test captures current behavior (uppercases name, generates ID, sends email). Refactoring: Extract EmailService, IDGenerator, NameFormatter. Tests still pass = behavior preserved. Benefits: Safe refactoring (tests catch regressions), documents legacy behavior (even bugs), enables SOLID migration (can refactor without breaking). Michael Feathers: 'Characterization tests describe what system actually does, not what it should do.' Approval testing: Record output as approved snapshot, verify future runs match. Tools: Jest snapshots, ApprovalTests library. Strategy: Write characterization tests for hot paths (frequently changed code), refactor towards SOLID (extract responsibilities), gradually replace characterization tests with focused unit tests. Modern: Snapshot testing (React components), golden file testing (CLI output).
Use differential coverage to detect OCP violations - adding feature should add tests, not modify existing test coverage. Process: (1) Baseline - measure code coverage before feature. (2) Add feature (new payment method). (3) Measure coverage again. (4) Analyze changes. OCP compliant: Coverage added in new files only (BitcoinProcessor.test.ts), existing files unchanged (PaymentService.test.ts). Violation: Existing test files modified to handle new case (if(type === 'bitcoin') in PaymentService.test.ts). Tools: Istanbul/nyc (JavaScript coverage), Codecov (track coverage over time), SonarQube (differential coverage reports). Metrics: (1) Coverage churn - % of existing coverage modified. OCP: <5%. (2) New vs modified lines - OCP: 90% new, 10% modified. (3) Test file edits - adding feature should create new test files, not edit existing. Example report: + BitcoinProcessor.test.ts (120 lines, 100% coverage). PaymentService.test.ts (unchanged). PaymentProcessor.test.ts (added 1 line for new type in shared test). Benefits: Quantifies OCP compliance, tracks architectural decay over time, enforces extensibility. Modern: GitHub/GitLab CI checks enforce coverage rules (no decrease in existing files, only additions). Pre-commit hooks: Fail if modifying >5% existing test lines for new feature.
Property-based testing generates random inputs to verify invariants - useful for testing LSP and interface contracts. Pattern: import {fc, test} from 'fast-check'; test('all Storage implementations preserve data', () => {fc.assert(fc.property(fc.string(), fc.string(), async (key, value) => {const storage = new LocalStorage(); await storage.save(key, value); const retrieved = await storage.get(key); expect(retrieved).toBe(value);}));}). Fast-check generates 100 random key/value pairs, verifies property holds for all. LSP testing: test('all implementations', () => {[LocalStorage, S3Storage, MemoryStorage].forEach(StorageClass => {fc.assert(fc.property(fc.string(), fc.string(), async (key, value) => {const storage = new StorageClass(); await storage.save(key, value); expect(await storage.get(key)).toBe(value);}));});}). Benefits: Tests edge cases (empty strings, special characters, very long values) automatically, finds bugs example-based tests miss, verifies contracts hold for all inputs. SOLID connection: LSP - verify all implementations satisfy same properties. ISP - property tests focus on minimal interface (only methods actually tested). OCP - adding implementation doesn't require new test cases (property tests apply automatically). Modern: Hypothesis (Python), QuickCheck (Haskell), fast-check (JavaScript). Strategies: Shrinking (find minimal failing case), custom generators (domain-specific values), statistical testing (distribution of generated values).
Architectural tests verify code structure and dependencies - automate SOLID rules as executable tests. Pattern: import {ArchUnit} from 'ts-arch'; test('Domain layer has no infrastructure dependencies', () => {const rule = filesOfPackage('domain').shouldNotDependOn('infrastructure'); expect(rule.check()).toPass();}). Tests enforce: (1) DIP - High-level modules don't depend on low-level. test('services depend on interfaces', () => {filesOfPackage('services').shouldDependOnlyOn('interfaces', 'domain');}). (2) SRP - Classes in layer have consistent dependencies. test('controllers depend only on use cases', () => {filesOfPackage('controllers').shouldDependOnlyOn('use-cases');}). (3) OCP - New features added without modifying core. test('core has no dependencies on features', () => {filesOfPackage('core').shouldNotDependOn('features');}). Tools: ArchUnit (Java), ts-arch (TypeScript), dependency-cruiser (JavaScript). Rules: No cycles, layer dependencies (UI → Domain → Infrastructure never reverse), naming conventions (services end with Service). Benefits: Architecture as code (rules executable, not just documentation), prevents drift (tests fail if architecture violated), onboarding (new devs see rules in tests). Modern: ESLint plugins (no-restricted-imports), import linters, module boundaries in monorepos (Nx). Example: test('no circular dependencies', () => {expect(analyzeCycles(project)).toHaveLength(0);}).
Test DI container configuration to verify correct wiring and SOLID principles. Pattern: describe('DI Container', () => {let container: Container; beforeEach(() => {container = new Container(); registerServices(container);}); test('resolves UserService with dependencies', () => {const service = container.get
Test brittleness: Tests break for unrelated changes - indicates tight coupling violating SOLID. Metrics: (1) Test churn rate - % of tests modified per code change. Brittle: >50%. SOLID: <20%. (2) Cascading failures - single code change breaks 10+ tests (tight coupling). (3) Test maintenance time - hours spent fixing tests vs implementing feature. Target: 1:2 ratio. Detection: test('creates user', () => {const db = new Database('localhost', 5432, 'prod', 'user', 'pass'); const email = new EmailService('smtp.gmail.com', 587, '[email protected]', 'pass'); const logger = new Logger('production', '/var/log'); const cache = new Cache('redis://localhost:6379'); const service = new UserService(db, email, logger, cache); service.create(user);}). Brittleness: Test knows concrete implementations (Database, EmailService), environment details (localhost, ports), configuration (production mode). Breaks when: DB host changes, email provider changes, log path changes, cache URL changes. SOLID solution: Mock interfaces - const mockDb: IDatabase = {save: jest.fn()}; const service = new UserService(mockDb). Benefits: Tests isolated (only break when UserService changes), fast (no real dependencies), flexible (swap implementations). Modern: Test builders, object mothers, factory functions reduce brittleness. Metric: Test pyramid - 70% unit (isolated), 20% integration, 10% E2E. Inverted pyramid = brittle tests.
Test React ISP via prop analysis - components should use all props received, not just subset. Pattern: test('Button uses all props', () => {const props = {onClick: jest.fn(), label: 'Click me', disabled: false, variant: 'primary'}; render(<Button {...props} />); const button = screen.getByRole('button'); expect(button).toHaveTextContent('Click me'); expect(button).toHaveClass('primary'); expect(button).not.toBeDisabled(); fireEvent.click(button); expect(props.onClick).toHaveBeenCalled();}). All props tested = no unused dependencies. ISP violation detection: test('Header receives user but only uses user.name', () => {const user = {name: 'Alice', email: '[email protected]', role: 'admin', createdAt: new Date()}; render(
Test SRP compliance via behavioral and structural indicators. Behavioral tests: (1) Count test file responsibilities - if testing UserService requires mocking database, email, validation, caching, logging = SRP violation (5 responsibilities). SRP compliant: Only mock user repository. (2) Change impact analysis - modify feature, count test files needing updates. >5 files = scattered responsibility. (3) Test setup complexity - if beforeEach() has 20 lines of mocks = too many dependencies. Structural tests: (1) Static analysis - class >500 lines, >20 methods flags SRP risk. (2) Import analysis - class importing from 5+ different domains (db, email, http, cache, crypto) = multiple responsibilities. (3) Method cohesion - LCOM >0.8 indicates low cohesion. Test pattern: describe('UserService', () => {const mockRepo = {save: jest.fn(), find: jest.fn()}; const service = new UserService(mockRepo); test('creates user', async () => {await service.create(userData); expect(mockRepo.save).toHaveBeenCalled();}); }). If test requires 10 mocks, extract responsibilities. Metrics: Cyclomatic complexity per method (<10), class size (<300 lines), number of dependencies (<5). Tools: SonarQube, CodeClimate flag SRP violations. TDD approach: Write test first - if hard to test, likely violates SRP.
Verify OCP via extension tests - add new behavior without modifying existing code or tests. Test pattern: Base tests remain unchanged when adding variants. Example: describe('PaymentProcessor', () => {testProcessor(new CreditCardProcessor()); testProcessor(new PayPalProcessor()); testProcessor(new BitcoinProcessor());}); function testProcessor(processor: PaymentProcessor) {test('processes valid payment', () => {expect(processor.process(100)).resolves.toBe(true);}); test('rejects invalid amount', () => {expect(processor.process(-1)).rejects.toThrow();});}}. OCP compliance: Adding BitcoinProcessor doesn't modify testProcessor() function or existing tests. Violations to detect: (1) Adding variant requires modifying existing test suite. (2) Tests with if(processor.type === 'credit') conditionals. (3) Tight coupling - tests know concrete implementations. Refactoring test indicator: Count modified lines when adding feature. OCP compliant: Add new test file for variant, zero lines changed in existing tests. Metrics: Test churn - files modified per feature. High churn = OCP violation. Modern: Parameterized tests (Jest describe.each, Vitest test.each) enable testing all variants with single test definition. Coverage: Each implementation has own test file + shared interface tests. Goal: Add BitcoinProcessor.test.ts, existing CreditCard.test.ts unchanged.
LSP testing: Write tests against base class, verify all subclasses pass identical tests without modification. Pattern: describe('Storage interface', () => {testStorage(new LocalStorage()); testStorage(new S3Storage()); testStorage(new MemoryStorage());}); function testStorage(storage: IStorage) {test('saves and retrieves', async () => {await storage.save('key', 'value'); const result = await storage.get('key'); expect(result).toBe('value');}); test('returns null for missing key', async () => {expect(await storage.get('nonexistent')).toBeNull();}); test('overwrites existing key', async () => {await storage.save('key', 'v1'); await storage.save('key', 'v2'); expect(await storage.get('key')).toBe('v2');});}}. LSP compliance: All storage implementations pass testStorage() without special cases. Violations: (1) if(storage instanceof S3Storage) skip test - S3Storage doesn't honor contract. (2) Different error types - S3 throws AwsError, Local throws FileError. (3) Different return types - S3 returns Buffer, Local returns string. Refactoring: Fix implementations to match contract. Benefits: Substitutability verified, behavioral compatibility guaranteed, safe polymorphism. Modern: Property-based testing (fast-check) generates random inputs, tests all implementations with same property. Coverage: 100% of base interface methods tested with all implementations.
Test ISP via mock analysis - count mocked methods vs used methods. Pattern: test('OrderService processes order', () => {const mockPayment = {process: jest.fn().mockResolvedValue(true), refund: jest.fn(), generateReport: jest.fn(), auditLog: jest.fn(), updateSettings: jest.fn()}; const service = new OrderService(mockPayment); service.processOrder(order); expect(mockPayment.process).toHaveBeenCalled(); // Only process() used, 4 methods stubbed unnecessarily}). ISP violation: OrderService depends on PaymentProcessor interface with 5 methods but uses only 1. Refactoring: Extract focused interface. interface PaymentExecutor {process(amount): Promise
Test DIP via dependency injection and test doubles - if can't inject mocks, violates DIP. Pattern: class OrderService {constructor(private db: IDatabase, private email: IEmailSender) {}}. Test: const mockDb: IDatabase = {save: jest.fn(), find: jest.fn()}; const mockEmail: IEmailSender = {send: jest.fn()}; const service = new OrderService(mockDb, mockEmail); await service.processOrder(order); expect(mockDb.save).toHaveBeenCalledWith(order); expect(mockEmail.send).toHaveBeenCalled(). DIP compliance: OrderService testable with mocks, no real database/email needed. Violations: (1) Can't test without real dependencies - class OrderService {private db = new MySQLDatabase()} forces integration test. (2) Hard to mock - Service Locator pattern requires global setup. (3) Concrete dependencies - new CreditCardProcessor() in code, can't swap. Test doubles: (1) Mock - verify interactions (expect(db.save).toHaveBeenCalled()). (2) Stub - provide canned responses (jest.fn().mockResolvedValue(user)). (3) Fake - working implementation (InMemoryDatabase for tests). (4) Spy - wrap real object to verify calls. Modern: Jest/Vitest automatic mocks, TypeScript jest.Mocked
Contract tests verify interface contracts are honored by all implementations - ensures LSP and ISP compliance. Pattern: interface IPaymentGateway {charge(amount: number): Promise
Mutation testing verifies test quality by introducing bugs (mutants) - helps detect untested SOLID violations. Process: (1) Tool mutates code (change > to >=, remove if condition, swap return values). (2) Run test suite against mutant. (3) If tests pass = escaped mutant = weak tests. Goal: Kill all mutants (tests fail for mutations). SOLID connection: Well-designed code (SOLID) easier to mutation test. SRP: Small focused classes have focused tests, easier to kill mutants. Example: class Validator {validate(email: string) {return /\S+@\S+/.test(email);}}. Mutation: return true (remove regex). Test: expect(validator.validate('invalid')).toBe(false) kills mutant. OCP: Mutation tests verify all implementations. Mutate StripeProcessor.charge(), tests for PayPal should pass (isolated), tests for Stripe should fail (specific). Tools: Stryker (JavaScript/TypeScript), PITest (Java). Configuration: stryker init, stryker run. Metrics: Mutation score = killed mutants / total mutants. Target: >80%. Benefits: Finds weak tests (assertions missing), verifies code actually tested (not just covered), improves test quality. Cost: Slow (runs tests N times for N mutants). Use on critical paths. Modern: Differential mutation testing (mutate only changed code), parallel execution (faster).
TDD enforces SRP by making design pain obvious during test writing - hard to test = SRP violation. Red-Green-Refactor with SRP: (1) Red - Write test for single responsibility. test('UserService creates user', () => {const service = new UserService(mockRepo); service.create(user); expect(mockRepo.save).toHaveBeenCalled();}). (2) Green - Implement minimum code to pass. class UserService {constructor(private repo: UserRepository) {} create(user: User) {this.repo.save(user);}}. (3) Refactor - Extract additional responsibilities. If service.create() also validates, sends email, logs - extract to separate classes. TDD signals for SRP violations: (1) Test setup is complex (10+ mocks needed). (2) Testing one method requires testing unrelated concerns. (3) Mockito/Jest mock hell (mocking mocks). (4) Can't name test clearly ('creates user and sends email and logs' = multiple responsibilities). Refactoring: test('UserService creates user', ...); test('EmailService sends welcome email', ...); test('Logger logs user creation', ...). Benefits: SRP violations caught at test-writing time (before implementation), forces thinking about responsibilities upfront, results in testable code. Modern: Outside-in TDD (start with acceptance test, drill down to unit tests) naturally creates focused responsibilities. Coverage: One test file per responsibility.
Characterization tests document existing behavior before refactoring - safety net for SOLID improvements to legacy code. Process: (1) Write tests describing current behavior (even if buggy). (2) Run tests, verify they pass. (3) Refactor towards SOLID. (4) Tests ensure behavior unchanged. Pattern: test('legacy UserService behavior', () => {const service = new UserService(); const result = service.processUser({name: 'Alice', email: '[email protected]'}); expect(result).toEqual({id: expect.any(Number), name: 'ALICE', email: '[email protected]', createdAt: expect.any(Date), welcomeEmailSent: true});}). Test captures current behavior (uppercases name, generates ID, sends email). Refactoring: Extract EmailService, IDGenerator, NameFormatter. Tests still pass = behavior preserved. Benefits: Safe refactoring (tests catch regressions), documents legacy behavior (even bugs), enables SOLID migration (can refactor without breaking). Michael Feathers: 'Characterization tests describe what system actually does, not what it should do.' Approval testing: Record output as approved snapshot, verify future runs match. Tools: Jest snapshots, ApprovalTests library. Strategy: Write characterization tests for hot paths (frequently changed code), refactor towards SOLID (extract responsibilities), gradually replace characterization tests with focused unit tests. Modern: Snapshot testing (React components), golden file testing (CLI output).
Use differential coverage to detect OCP violations - adding feature should add tests, not modify existing test coverage. Process: (1) Baseline - measure code coverage before feature. (2) Add feature (new payment method). (3) Measure coverage again. (4) Analyze changes. OCP compliant: Coverage added in new files only (BitcoinProcessor.test.ts), existing files unchanged (PaymentService.test.ts). Violation: Existing test files modified to handle new case (if(type === 'bitcoin') in PaymentService.test.ts). Tools: Istanbul/nyc (JavaScript coverage), Codecov (track coverage over time), SonarQube (differential coverage reports). Metrics: (1) Coverage churn - % of existing coverage modified. OCP: <5%. (2) New vs modified lines - OCP: 90% new, 10% modified. (3) Test file edits - adding feature should create new test files, not edit existing. Example report: + BitcoinProcessor.test.ts (120 lines, 100% coverage). PaymentService.test.ts (unchanged). PaymentProcessor.test.ts (added 1 line for new type in shared test). Benefits: Quantifies OCP compliance, tracks architectural decay over time, enforces extensibility. Modern: GitHub/GitLab CI checks enforce coverage rules (no decrease in existing files, only additions). Pre-commit hooks: Fail if modifying >5% existing test lines for new feature.
Property-based testing generates random inputs to verify invariants - useful for testing LSP and interface contracts. Pattern: import {fc, test} from 'fast-check'; test('all Storage implementations preserve data', () => {fc.assert(fc.property(fc.string(), fc.string(), async (key, value) => {const storage = new LocalStorage(); await storage.save(key, value); const retrieved = await storage.get(key); expect(retrieved).toBe(value);}));}). Fast-check generates 100 random key/value pairs, verifies property holds for all. LSP testing: test('all implementations', () => {[LocalStorage, S3Storage, MemoryStorage].forEach(StorageClass => {fc.assert(fc.property(fc.string(), fc.string(), async (key, value) => {const storage = new StorageClass(); await storage.save(key, value); expect(await storage.get(key)).toBe(value);}));});}). Benefits: Tests edge cases (empty strings, special characters, very long values) automatically, finds bugs example-based tests miss, verifies contracts hold for all inputs. SOLID connection: LSP - verify all implementations satisfy same properties. ISP - property tests focus on minimal interface (only methods actually tested). OCP - adding implementation doesn't require new test cases (property tests apply automatically). Modern: Hypothesis (Python), QuickCheck (Haskell), fast-check (JavaScript). Strategies: Shrinking (find minimal failing case), custom generators (domain-specific values), statistical testing (distribution of generated values).
Architectural tests verify code structure and dependencies - automate SOLID rules as executable tests. Pattern: import {ArchUnit} from 'ts-arch'; test('Domain layer has no infrastructure dependencies', () => {const rule = filesOfPackage('domain').shouldNotDependOn('infrastructure'); expect(rule.check()).toPass();}). Tests enforce: (1) DIP - High-level modules don't depend on low-level. test('services depend on interfaces', () => {filesOfPackage('services').shouldDependOnlyOn('interfaces', 'domain');}). (2) SRP - Classes in layer have consistent dependencies. test('controllers depend only on use cases', () => {filesOfPackage('controllers').shouldDependOnlyOn('use-cases');}). (3) OCP - New features added without modifying core. test('core has no dependencies on features', () => {filesOfPackage('core').shouldNotDependOn('features');}). Tools: ArchUnit (Java), ts-arch (TypeScript), dependency-cruiser (JavaScript). Rules: No cycles, layer dependencies (UI → Domain → Infrastructure never reverse), naming conventions (services end with Service). Benefits: Architecture as code (rules executable, not just documentation), prevents drift (tests fail if architecture violated), onboarding (new devs see rules in tests). Modern: ESLint plugins (no-restricted-imports), import linters, module boundaries in monorepos (Nx). Example: test('no circular dependencies', () => {expect(analyzeCycles(project)).toHaveLength(0);}).
Test DI container configuration to verify correct wiring and SOLID principles. Pattern: describe('DI Container', () => {let container: Container; beforeEach(() => {container = new Container(); registerServices(container);}); test('resolves UserService with dependencies', () => {const service = container.get
Test brittleness: Tests break for unrelated changes - indicates tight coupling violating SOLID. Metrics: (1) Test churn rate - % of tests modified per code change. Brittle: >50%. SOLID: <20%. (2) Cascading failures - single code change breaks 10+ tests (tight coupling). (3) Test maintenance time - hours spent fixing tests vs implementing feature. Target: 1:2 ratio. Detection: test('creates user', () => {const db = new Database('localhost', 5432, 'prod', 'user', 'pass'); const email = new EmailService('smtp.gmail.com', 587, '[email protected]', 'pass'); const logger = new Logger('production', '/var/log'); const cache = new Cache('redis://localhost:6379'); const service = new UserService(db, email, logger, cache); service.create(user);}). Brittleness: Test knows concrete implementations (Database, EmailService), environment details (localhost, ports), configuration (production mode). Breaks when: DB host changes, email provider changes, log path changes, cache URL changes. SOLID solution: Mock interfaces - const mockDb: IDatabase = {save: jest.fn()}; const service = new UserService(mockDb). Benefits: Tests isolated (only break when UserService changes), fast (no real dependencies), flexible (swap implementations). Modern: Test builders, object mothers, factory functions reduce brittleness. Metric: Test pyramid - 70% unit (isolated), 20% integration, 10% E2E. Inverted pyramid = brittle tests.
Test React ISP via prop analysis - components should use all props received, not just subset. Pattern: test('Button uses all props', () => {const props = {onClick: jest.fn(), label: 'Click me', disabled: false, variant: 'primary'}; render(<Button {...props} />); const button = screen.getByRole('button'); expect(button).toHaveTextContent('Click me'); expect(button).toHaveClass('primary'); expect(button).not.toBeDisabled(); fireEvent.click(button); expect(props.onClick).toHaveBeenCalled();}). All props tested = no unused dependencies. ISP violation detection: test('Header receives user but only uses user.name', () => {const user = {name: 'Alice', email: '[email protected]', role: 'admin', createdAt: new Date()}; render(