Testing private methods is one of those topics that can easily become more philosophical than practical.
In theory, private methods should not be tested directly. They are implementation details, and unit tests should verify the public behavior of a class. If a private method is changed, renamed, removed, split, or merged, the tests should usually not care, as long as the observable behavior of the class remains correct.
That is the clean answer.
Real code, however, is not always clean.
Sometimes you inherit a large class that was not designed with testability in mind. Sometimes the public method does too much, talks to too many dependencies, or is too expensive to exercise in a simple unit test. Sometimes a private method contains an important algorithm that you need to protect with tests before you can safely refactor the code.
In those cases, testing a private method can be a necessary evil.
The key is to treat it as a tactical compromise, not as the default way to write unit tests.
The normal rule: test behavior, not implementation
A private method is private for a reason: it belongs to the internal implementation of a class.
Consider a class like this:
public sealed class InvoiceCalculator
{
public decimal CalculateTotal(Invoice invoice)
{
decimal subtotal = CalculateSubtotal(invoice);
decimal discount = CalculateDiscount(invoice, subtotal);
decimal tax = CalculateTax(invoice, subtotal - discount);
return subtotal - discount + tax;
}
private decimal CalculateSubtotal(Invoice invoice)
{
return invoice.Lines.Sum(line => line.Quantity * line.UnitPrice);
}
private decimal CalculateDiscount(Invoice invoice, decimal subtotal)
{
if (invoice.Customer.IsPreferred)
{
return subtotal * 0.10m;
}
return 0m;
}
private decimal CalculateTax(Invoice invoice, decimal taxableAmount)
{
return taxableAmount * invoice.TaxRate;
}
}
The useful contract of this class is not CalculateSubtotal, CalculateDiscount, or CalculateTax. The useful contract is CalculateTotal.
A good test should normally look like this:
[TestMethod]
public void CalculateTotal_applies_preferred_customer_discount_before_tax()
{
var invoice = new Invoice
{
Customer = new Customer { IsPreferred = true },
TaxRate = 0.20m,
Lines =
[
new InvoiceLine { Quantity = 2, UnitPrice = 100m }
]
};
var calculator = new InvoiceCalculator();
decimal total = calculator.CalculateTotal(invoice);
Assert.AreEqual(216m, total);
}
This test does not care how the class is internally structured. The private methods could be renamed, merged, extracted, or rewritten, and the test would still describe the behavior that matters.
That is what makes tests valuable during refactoring: they protect the contract without freezing the implementation.
Why direct tests of private methods are fragile
Testing private methods directly has several downsides.
First, it couples the test to the internal structure of the class. A private method name becomes part of the test, even though it is not part of the public API.
Second, it can make refactoring harder. A refactoring that should be safe may break tests simply because a private method was renamed or split into smaller helpers.
Third, it can lead to duplicated coverage. If the public method already exercises the private method, direct tests may end up testing the same behavior twice: once through the public API and once through the implementation detail.
Finally, it can hide a design problem. If a private method is complex enough to deserve its own dedicated test suite, it may actually be a separate responsibility that belongs in another class.
That does not mean direct testing is always wrong. It means it should make you stop and ask why the private method is so important.
When testing a private method can be justified
There are cases where testing a private method directly is reasonable.
The most common one is legacy code.
Imagine a class that loads data, talks to external services, transforms records, updates state, writes logs, and returns a result. The class may have one public method and several private methods, some of which contain non-trivial business logic.
In an ideal world, you would refactor the class first.
In the real world, refactoring without tests may be too risky. You may need a few characterization tests around the existing private logic before changing anything.
Testing a private method can be justified when:
- the class is legacy code;
- the public method is too broad, slow, or hard to set up;
- the private method contains important logic;
- you need tests before refactoring;
- the test is temporary or clearly understood as technical debt.
This is the important distinction: testing a private method is not the destination. It is a bridge that helps you reach a better design safely.
The better option: extract the logic into a separate class
If a private method contains meaningful business logic, the best solution is often to extract that logic into a dedicated class.
Instead of this:
public sealed class InvoiceCalculator
{
public decimal CalculateTotal(Invoice invoice)
{
decimal discount = CalculateDiscount(invoice);
// More calculation logic...
}
private decimal CalculateDiscount(Invoice invoice)
{
if (invoice.Customer.IsPreferred)
{
return invoice.Subtotal * 0.10m;
}
return 0m;
}
}
You can move the discount logic into a separate component:
public sealed class DiscountCalculator
{
public decimal CalculateDiscount(Invoice invoice)
{
if (invoice.Customer.IsPreferred)
{
return invoice.Subtotal * 0.10m;
}
return 0m;
}
}
Now the logic can be tested directly through a public method:
[TestMethod]
public void CalculateDiscount_returns_10_percent_for_preferred_customer()
{
var invoice = new Invoice
{
Subtotal = 200m,
Customer = new Customer { IsPreferred = true }
};
var calculator = new DiscountCalculator();
decimal discount = calculator.CalculateDiscount(invoice);
Assert.AreEqual(20m, discount);
}
This is cleaner than testing a private method because the test now describes a real contract.
It also improves the production code. The original class becomes smaller, the responsibility becomes clearer, and the extracted logic becomes reusable.
A private method that wants its own tests is often a class trying to be born.
Another option: make the member internal
Sometimes a method is not really part of the public API, but it is still useful to test it directly. In that case, making it internal can be a pragmatic compromise.
For example:
public sealed class InvoiceCalculator
{
public decimal CalculateTotal(Invoice invoice)
{
decimal discount = CalculateDiscount(invoice);
// More calculation logic...
}
internal decimal CalculateDiscount(Invoice invoice)
{
if (invoice.Customer.IsPreferred)
{
return invoice.Subtotal * 0.10m;
}
return 0m;
}
}
Then expose internal members to the test project using InternalsVisibleTo:
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo(“MyProject.Tests”)]
Now the test project can call the internal method without reflection:
[TestMethod]
public void CalculateDiscount_returns_10_percent_for_preferred_customer()
{
var invoice = new Invoice
{
Subtotal = 200m,
Customer = new Customer { IsPreferred = true }
};
var calculator = new InvoiceCalculator();
decimal discount = calculator.CalculateDiscount(invoice);
Assert.AreEqual(20m, discount);
}
This approach is usually better than reflection because it is strongly typed. If the method is renamed, the test fails at compile time instead of failing at runtime.
However, this approach should also be used with care. If you make many members internal only for testing, the production assembly can become cluttered with methods that are not part of its real design.
Use internal when the method represents a meaningful unit of behavior inside the assembly, not just because a private method is inconvenient to reach.
The direct option: use MSTest PrivateObject
When you really need to call a private instance method directly in MSTest, PrivateObject can be used as a reflection wrapper.
Suppose you have a class like this:
public sealed class DefaultDataModel
{
private readonly IDataReader dataReader;
public DefaultDataModel(IDataReader dataReader)
{
this.dataReader = dataReader;
}
public IReadOnlyList<LPRead> LoadValidReads()
{
List<LPRead> reads = GetReads();
return reads
.Where(read => read.IsValid)
.ToList();
}
private List<LPRead> GetReads()
{
return dataReader.ReadAll().ToList();
}
}
Normally, you should test LoadValidReads, because that is the public behavior.
But if this is legacy code and you need to test GetReads directly before refactoring, MSTest lets you do this:
[TestMethod]
public void GetReads_returns_reads_from_data_reader()
{
var dataReader = A.Fake<IDataReader>();
A.CallTo(() => dataReader.ReadAll())
.Returns(
[
new LPRead { Id = 1, IsValid = true },
new LPRead { Id = 2, IsValid = false }
]);
var model = new DefaultDataModel(dataReader);
var privateObject = new PrivateObject(model);
var reads = (List<LPRead>)privateObject.Invoke("GetReads");
Assert.AreEqual(2, reads.Count);
Assert.AreEqual(1, reads[0].Id);
Assert.AreEqual(2, reads[1].Id);
}
The important line is this one:
var privateObject = new PrivateObject(model);
PrivateObject wraps the instance under test. Then the private method can be invoked by name:
var reads = (List<LPRead>)privateObject.Invoke("GetReads");
This works, but it has obvious drawbacks.
The method name is a string. The return value must be cast. If the method signature changes, the failure happens at runtime. The test also knows about a private implementation detail that production code cannot call.
For that reason, this technique should be used sparingly.
Calling a private static method
For private static methods, MSTest provides a similar idea through PrivateType.
For example:
public static class FileNameParser
{
public static ParsedFileName Parse(string fileName)
{
string normalized = Normalize(fileName);
// More parsing logic...
return new ParsedFileName(normalized);
}
private static string Normalize(string fileName)
{
return fileName.Trim().ToUpperInvariant();
}
}
A direct test of the private static method would look like this:
[TestMethod]
public void Normalize_trims_and_uppercases_file_name()
{
var privateType = new PrivateType(typeof(FileNameParser));
var result = (string)privateType.InvokeStatic(
"Normalize",
" invoice-001.csv ");
Assert.AreEqual("INVOICE-001.CSV", result);
}
Again, this should not be the first choice. If Normalize is important enough to be tested directly, it may belong in a separate class, perhaps as part of a filename normalization service.
Using reflection directly
If you are not using MSTest, or you do not want to depend on PrivateObject, you can call private methods through reflection.
[TestMethod]
public void GetReads_returns_reads_from_data_reader()
{
var dataReader = A.Fake<IDataReader>();
A.CallTo(() => dataReader.ReadAll())
.Returns(
[
new LPRead { Id = 1, IsValid = true },
new LPRead { Id = 2, IsValid = false }
]);
var model = new DefaultDataModel(dataReader);
var method = typeof(DefaultDataModel).GetMethod(
"GetReads",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(method);
var reads = (List<LPRead>)method!.Invoke(model, null)!;
Assert.AreEqual(2, reads.Count);
}
This is more verbose than PrivateObject, but it makes the mechanism explicit.
It also makes clear what is really happening: you are bypassing normal visibility rules.
That is sometimes useful, but it should feel slightly uncomfortable. The discomfort is a signal that this is not the ideal design.
A useful decision checklist
Before testing a private method directly, ask these questions.
- Can I test the same behavior through a public method? If yes, prefer the public method.
- Is the private method complex because the class has too many responsibilities? If yes, consider extracting a new class.
- Is the method not really private, but only not part of the public API? If yes, consider making it
internaland usingInternalsVisibleTo. - Am I working with legacy code that cannot be safely refactored yet? If yes, a direct private method test may be acceptable as temporary protection.
- Will this test make future refactoring harder? If yes, keep the test narrow and consider replacing it after refactoring.
Do not make methods public only for tests
One thing to avoid is changing a method from private to public only because a test needs to call it. That changes the production API. A public method communicates that other code is allowed to call it. Once a method is public, it becomes harder to change or remove because other parts of the codebase may start depending on it.
If the method is not intended for production callers, do not make it public just for tests.
Prefer one of these options instead:
- test through the public behavior;
- extract the logic into a new class;
- make the method
internalif it is a meaningful assembly-level concept; - use reflection or
PrivateObjectonly as a short-term legacy-code compromise.
A practical rule of thumb
For new code, do not test private methods directly.
For well-designed code, test public behavior.
For complex private logic, extract a separate class and test that class.
For legacy code, testing a private method can be acceptable when it helps you create a safety net before refactoring.
That last point is why testing private methods is a necessary evil. It is not something to celebrate, but it is sometimes the safest next step.
The important thing is to know when you are making a compromise.
If the test helps you safely improve the design, it is doing its job.
If the test freezes a poor design in place, it is making the problem worse.


