# Testing with Murphy Guide to testing code that uses Murphy policies, including unit testing strategies and test mode. ## Test Mode Murphy includes a global test mode flag that disables actual delays, making tests run instantly. ### Enabling Test Mode ```pascal uses Murphy.Globals; procedure TMyTests.SetUp; begin MurphyTestModeEnabled := True; end; procedure TMyTests.TearDown; begin MurphyTestModeEnabled := False; end; ``` ### What Test Mode Does - **Retry policy**: `Wait()` delays are skipped - **Circuit breaker**: `Within()` duration checks use real time, but operations complete instantly - **Rate limit**: Operations complete instantly (but rate limiting still enforces limits) ## Unit Testing Patterns ### Testing Retry Behavior ```pascal procedure TRetryTests.TestRetryExecutesMultipleTimes; var Policy: IRetryPolicy; AttemptCount: Integer; begin MurphyTestModeEnabled := True; AttemptCount := 0; Policy := TRetryBuilder .Handle(Exception) .Retry(3) .Build; try Policy.Execute( procedure begin Inc(AttemptCount); if AttemptCount < 3 then raise Exception.Create('Simulated failure'); // Third attempt succeeds end); Assert.AreEqual(3, AttemptCount, 'Should have attempted 3 times'); finally MurphyTestModeEnabled := False; end; end; ``` ### Testing Circuit Breaker States ```pascal procedure TCircuitBreakerTests.TestCircuitOpensAfterThreshold; var Policy: ICircuitBreakerPolicy; I: Integer; begin MurphyTestModeEnabled := True; Policy := TCircuitBreakerBuilder .Handle(Exception) .Fail(3) .Build; // Trigger 3 failures to open circuit for I := 1 to 3 do begin try Policy.Execute(procedure begin raise Exception.Create('Test'); end); except // Expected end; end; Assert.AreEqual(TCircuitState.Open, Policy.CircuitState); MurphyTestModeEnabled := False; end; ``` ### Testing Fallback Behavior ```pascal procedure TFallbackTests.TestFallbackReturnsDefaultOnException; var Policy: IFallbackPolicy; Result: string; begin Policy := TFallbackBuilder .Handle(Exception) .Fallback(function: string begin Result := 'Fallback Value'; end) .Build; Result := Policy.Execute( function: string begin raise Exception.Create('Test error'); end); Assert.AreEqual('Fallback Value', Result); end; ``` ### Testing Rate Limit ```pascal procedure TRateLimitTests.TestRateLimitRejectsExcessCalls; var Policy: IRateLimitPolicy; I: Integer; RejectedCount: Integer; begin MurphyTestModeEnabled := True; RejectedCount := 0; Policy := TRateLimitBuilder .Handle(Exception) .Allow(5) .Within(TTimeSpan.FromSeconds(1)) .Build; // Try 10 calls, expect 5 rejections for I := 1 to 10 do begin try Policy.Execute(procedure begin {do nothing} end); except on E: ERateLimitRejectedException do Inc(RejectedCount); end; end; Assert.AreEqual(5, RejectedCount); MurphyTestModeEnabled := False; end; ``` ## Mocking Policies You can mock policy interfaces for testing code that depends on them: ```pascal type TMockRetryPolicy = class(TInterfacedObject, IRetryPolicy) public ExecuteCount: Integer; procedure Execute(AProc: TProc); // ... other interface methods end; procedure TMockRetryPolicy.Execute(AProc: TProc); begin Inc(ExecuteCount); AProc; // Always succeeds end; procedure TServiceTests.TestServiceUsesRetry; var MockPolicy: TMockRetryPolicy; Service: TMyService; begin MockPolicy := TMockRetryPolicy.Create; Service := TMyService.Create(MockPolicy); Service.PerformOperation; Assert.IsTrue(MockPolicy.ExecuteCount > 0, 'Should have used retry policy'); end; ``` ## Testing Combined Policies ```pascal procedure TestRetryWithFallback; var Retry: IRetryPolicy; Fallback: IFallbackPolicy; AttemptCount: Integer; Result: string; begin MurphyTestModeEnabled := True; AttemptCount := 0; Retry := TRetryBuilder.Handle(Exception).Retry(2).Build; Fallback := TFallbackBuilder .Handle(Exception) .Fallback(function: string begin Result := 'Fallback'; end) .Build; Result := Fallback.Execute( function: string begin Retry.Execute( procedure begin Inc(AttemptCount); raise Exception.Create('Always fails'); end); end); Assert.AreEqual(3, AttemptCount, 'Should retry 2 times (3 total attempts)'); Assert.AreEqual('Fallback', Result, 'Should return fallback value'); MurphyTestModeEnabled := False; end; ``` ## Best Practices 1. **Always use test mode** - Tests will run much faster 2. **Test failure scenarios** - Not just happy paths 3. **Verify attempt counts** - Ensure retry logic works 4. **Test state transitions** - For circuit breaker 5. **Clean up test mode** - Reset in TearDown to avoid affecting other tests 6. **Use DUnitX/DUnit** - Standard Delphi testing frameworks work well 7. **Test policy combinations** - Ensure they interact correctly ## Integration Testing For integration tests (testing with real delays): ```pascal procedure TIntegrationTests.TestRetryWithRealDelay; var Policy: IRetryPolicy; StartTime, EndTime: TDateTime; begin MurphyTestModeEnabled := False; // Use real delays Policy := TRetryBuilder .Handle(Exception) .Retry(2) .Wait(TTimeSpan.FromMilliseconds(100)) .Build; StartTime := Now; try Policy.Execute( procedure begin raise Exception.Create('Always fails'); end); except // Expected end; EndTime := Now; var ElapsedMs := MilliSecondsBetween(EndTime, StartTime); Assert.IsTrue(ElapsedMs >= 200, 'Should have waited at least 200ms'); end; ``` ## Example: Full Test Suite ```pascal unit Murphy.Tests.Retry; interface uses DUnitX.TestFramework, Murphy.Policy.Retry, Murphy.Globals; type [TestFixture] TRetryPolicyTests = class public [Setup] procedure SetUp; [TearDown] procedure TearDown; [Test] procedure TestSuccessOnFirstAttempt; [Test] procedure TestRetryUntilSuccess; [Test] procedure TestExceptionRaisedAfterRetriesExhausted; [Test] procedure TestUnhandledExceptionNotRetried; end; implementation procedure TRetryPolicyTests.SetUp; begin MurphyTestModeEnabled := True; end; procedure TRetryPolicyTests.TearDown; begin MurphyTestModeEnabled := False; end; procedure TRetryPolicyTests.TestSuccessOnFirstAttempt; var Policy: IRetryPolicy; Executed: Boolean; begin Policy := TRetryBuilder.Handle(Exception).Retry(3).Build; Executed := False; Policy.Execute(procedure begin Executed := True; end); Assert.IsTrue(Executed); end; procedure TRetryPolicyTests.TestRetryUntilSuccess; var Policy: IRetryPolicy; Attempts: Integer; begin Policy := TRetryBuilder.Handle(Exception).Retry(3).Build; Attempts := 0; Policy.Execute( procedure begin Inc(Attempts); if Attempts < 3 then raise Exception.Create('Fail'); end); Assert.AreEqual(3, Attempts); end; procedure TRetryPolicyTests.TestExceptionRaisedAfterRetriesExhausted; var Policy: IRetryPolicy; begin Policy := TRetryBuilder.Handle(Exception).Retry(2).Build; Assert.WillRaise( procedure begin Policy.Execute(procedure begin raise Exception.Create('Always fails'); end); end, Exception); end; procedure TRetryPolicyTests.TestUnhandledExceptionNotRetried; var Policy: IRetryPolicy; Attempts: Integer; begin Policy := TRetryBuilder.Handle(EIdHTTPProtocolException).Retry(3).Build; Attempts := 0; Assert.WillRaise( procedure begin Policy.Execute( procedure begin Inc(Attempts); raise EDatabaseError.Create('Not handled'); end); end, EDatabaseError); Assert.AreEqual(1, Attempts, 'Should not retry unhandled exception'); end; end. ``` --- [Back to Index](../Home.md)