Unit Tests Over Protected and Private Methods
By definition, it is not possible to access externally private or protected methods.
And there are design reasons for this.
However, with some frequency, we may need to test those methods, and testing them individually can make sense.
When I need to do so, I always use reflection, which allows us to programmatically access the class structure and perform operations with it.
Here is what GhatGPT defines as Reflection in C#:
“Reflection in C# is a feature that allows code to inspect and interact with its own structure—such as assemblies, types, methods, and properties—at runtime. It enables dynamic type creation, method invocation, and metadata access, commonly used for plugins, serialization, and testing frameworks.”
In this example, we will create a Wrapper and wrap the reflection code inside.
Here is the definition of Wrapper, from ChatGPT:
"The Wrapper pattern, also known as the Adapter pattern, encapsulates an object to provide a different interface without modifying its code. It allows incompatible interfaces to work together by translating calls, often used to integrate legacy code or external libraries into a new system."
First, let's create our class that contains the private method that we are going to test.
For this example, let's create a class for an Invoice Item that contains Quantity and Unit Price.
Let’s assume that the subtotal is kept private, and there is a static method that calculates the subtotal, considering a tax of 10%.
Recommended by LinkedIn
public class InvoiceItem
{
private decimal _quantity;
public decimal Quantity
{
get => _quantity;
set
{
_quantity = value;
_subTotal = CalculateSubtotalWithTax(Quantity, UnitPrice);
}
}
private decimal _unitPrice;
public decimal UnitPrice
{
get => _unitPrice;
set
{
_unitPrice = value;
_subTotal = CalculateSubtotalWithTax(Quantity, UnitPrice);
}
}
private decimal _subTotal;
public override string ToString() => $"Subtotal: {_subTotal}";
private static decimal CalculateSubtotalWithTax(decimal quantity, decimal unitPrice)
{
var taxPercent = 10;
var result = quantity * unitPrice;
result += result * taxPercent / 100;
return result;
}
}
And here, the wrapper class encapsulates the code that uses reflection to call the private elements.
It is not necessary at all, but it makes it easy, organized, and centralized access to the private elements inside the class.
It receives the instance from InvoiceItem, in the constructor, and the GetSubtotal method will get the value from the _subTotal field, which is private.
Also, it has the CalculateSubtotalWithTax method, which is static, and calls the private method with the same name inside InvoiceItem class, which is also static
public class InvoiceItemWrapper
{
public InvoiceItemWrapper(InvoiceItem item)
{
_item = item;
}
private readonly InvoiceItem _item;
public decimal GetSubtotal()
{
var type = typeof(InvoiceItem);
var field = type.GetField("_subTotal", BindingFlags.NonPublic | BindingFlags.Instance);
var response = field.GetValue(_item);
return (decimal)response;
}
public static decimal CalculateSubtotalWithTax(decimal quantity, decimal unitPrice)
{
var type = typeof(InvoiceItem);
var method = type.GetMethod("CalculateSubtotalWithTax", BindingFlags.NonPublic | BindingFlags.Static);
var response = method.Invoke(null, new object?[] { quantity, unitPrice });
return (decimal)response;
}
}
And, for last, the test cases here, the first tests the over the calculation method, which has a tax of 10%. It uses the wrapper previously implemented.
The second method accesses the private field and gets its value.
public class PrivateMethodsTest
{
[Test]
public void PrivateMethods_StaticMethod()
{
var subtotal = InvoiceItemWrapper.CalculateSubtotalWithTax(10, 5);
Assert.AreEqual(55m, subtotal);
}
[Test]
public void PrivateMethods_PrivateField()
{
var item = new InvoiceItem();
item.Quantity = 10;
item.UnitPrice = 5;
var wrapper = new InvoiceItemWrapper(item);
var subtotal = wrapper.GetSubtotal();
Assert.AreEqual(55m, subtotal);
}
}
And here is the outcome:
#CSharp | #TDD | #dotNET | #NUnit
Senior Mobile Developer | Android Software Engineer | Jetpack Compose | GraphQL | Kotlin | Java | React Native | Swift
1wGreat article!
Analytics Engineer | Data Engineer | Data Analyst | Business Data Analyst
1wThis is a clever approach to testing private methods using reflection and wrappers! I’ve worked with teams where testing private methods was essential for ensuring edge cases were covered. While it’s not always ideal, this method provides a clean way to handle it. How do you balance maintaining encapsulation while ensuring thorough test coverage? #CSharp #TDD #dotNET
Software Engineer@TRT15 | JAVA | ANGULAR | AWS | SPRING | JBOSS
1wInteressante
FullStack Software Engineer | Java | AWS | Kafka | React JS | APIs
1wThanks for sharing, Cassio
Generative AI | LLMs | AI Solutions Architecture | RAG | MLOps & AIOps | Golang | Kotlin | Flutter | Java | .NET 8+ | Hexagonal Architecture | gRPC | Docker | Kubernetes | Terraform | Vertex AI | Multicloud AWS GCP Azure
1wThanks for sharing, Cassio