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
Solid Testing Strategies FAQ & Answers
15 expert Solid Testing Strategies answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
15 questionsTDD 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.
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(
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.
Contract tests verify interface contracts are honored by all implementations - ensures LSP and ISP compliance. Pattern: interface IPaymentGateway {charge(amount: number): Promise
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);}).
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).
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).
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.
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.
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).
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 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.
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
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.