Protected Methods

February 18, 2026 · View on GitHub

Home > Guides > Protected Methods

Protected Methods

KnockOff generates public interceptors for protected members on class stubs. You configure, verify, and sequence protected methods using the same API as public methods — fully compile-time safe.

This applies to class stubs only (Patterns 3, 4, 6, 9). Interface stubs have no access modifiers.


The Comparison

The scenario

A base class with a protected abstract method used by a public template method:

public abstract class ServiceBase
{
    protected virtual string Tag { get; set; } = "";
    protected abstract string GetInternalId();
    protected virtual string FormatTag() => $"[{Tag}]";

    public string GetDescription() => $"{GetInternalId()}: {FormatTag()}";
}

KnockOff

The stub wrapper exposes all interceptors — including protected ones — as public properties. Same Return/Call/Verify API:

var stub = new Stubs.ServiceBase();

// Configure protected abstract method via public interceptor
stub.GetInternalId.Return("ID-42");

ServiceBase service = stub.Object;
var desc = service.GetDescription();
// desc == "ID-42: []"

// Verify it was called
stub.GetInternalId.Verify(Called.Once);

Protected virtual methods fall back to the base implementation when unconfigured:

var stub = new Stubs.ServiceBase();
stub.Tag.Get("myTag");
stub.GetInternalId.Return("id");

// FormatTag is NOT configured — falls back to base: $"[{Tag}]"
ServiceBase service = stub.Object;
var desc = service.GetDescription();
// desc == "id: [myTag]"

Configure to override the base:

stub.FormatTag.Return("custom-format");

Sequences work too:

stub.GetInternalId
    .Call(() => "first-id")
    .ThenReturn(() => "second-id");

All member types are supported: methods, properties, indexers, and events.

Moq

Moq provides Mock.Protected() which uses string-based method names:

var mock = new Mock<ServiceBase>();

// String-based — no compile-time safety
mock.Protected()
    .Setup<string>("GetInternalId")
    .Returns("ID-42");

// Different matcher syntax: ItExpr instead of It
mock.Protected()
    .Setup<string>("FormatTag")
    .Returns("custom-format");

ServiceBase service = mock.Object;
var desc = service.GetDescription();

// Verify with string name
mock.Protected()
    .Verify("GetInternalId", Times.Once());

Pain points:

  • Method names are strings — rename the method and tests still compile but fail at runtime
  • Must use ItExpr instead of It for argument matching
  • Alternatively, define a mapping interface and use mock.Protected().As<IMapping>() — extra boilerplate

NSubstitute

NSubstitute has no built-in support for protected members. The workaround is a manual test subclass:

// Manual workaround: create a test subclass
public class TestServiceBase : ServiceBase
{
    public string GetInternalIdResult { get; set; } = "";

    protected override string GetInternalId() => GetInternalIdResult;
}

// Usage
var testService = new TestServiceBase { GetInternalIdResult = "ID-42" };
var desc = testService.GetDescription();

// No built-in verification — you'd need to add your own tracking

Pain points:

  • Must create a manual subclass for every base class you want to test
  • No verification support
  • No sequencing
  • Each protected member needs manual plumbing

Summary

CapabilityKnockOffMoqNSubstitute
Configure protected methodsstub.GetInternalId.Return(...)mock.Protected().Setup<T>("name")Manual subclass
Compile-time safetyYes — interceptor is typedNo — string method namesN/A
Verificationstub.GetInternalId.Verify(Called.Once)mock.Protected().Verify("name", Times.Once())Not available
Sequences.Return().ThenReturn()Limited (Moq 4.18+, string-based)Not available
Virtual base fallbackAutomatic when unconfiguredMust configure explicitlyManual in subclass
Properties / Indexers / EventsSame APILimited Protected() supportManual in subclass