Skip to content

Testing

Marco Breveglieri edited this page Nov 24, 2025 · 1 revision

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

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

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

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

procedure TFallbackTests.TestFallbackReturnsDefaultOnException;
var
  Policy: IFallbackPolicy<string>;
  Result: string;
begin
  Policy := TFallbackBuilder<string>
    .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

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:

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

procedure TestRetryWithFallback;
var
  Retry: IRetryPolicy;
  Fallback: IFallbackPolicy<string>;
  AttemptCount: Integer;
  Result: string;
begin
  MurphyTestModeEnabled := True;
  AttemptCount := 0;

  Retry := TRetryBuilder.Handle(Exception).Retry(2).Build;
  Fallback := TFallbackBuilder<string>
    .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):

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

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

Clone this wiki locally