Unit Tests Over Protected and Private Methods

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%.

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:

Article content

#CSharp | #TDD | #dotNET | #NUnit


Thiago Nunes Monteiro

Senior Mobile Developer | Android Software Engineer | Jetpack Compose | GraphQL | Kotlin | Java | React Native | Swift

1w

Great article!

Like
Reply
Rodrigo Modesto

Analytics Engineer | Data Engineer | Data Analyst | Business Data Analyst

1w

This 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

Like
Reply
Vinicius Martins

Software Engineer@TRT15 | JAVA | ANGULAR | AWS | SPRING | JBOSS

1w

Interessante

Like
Reply
Anderson Meurer

FullStack Software Engineer | Java | AWS | Kafka | React JS | APIs

1w

Thanks for sharing, Cassio

Like
Reply
Kleber Augusto dos Santos

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

1w

Thanks for sharing, Cassio

Like
Reply

To view or add a comment, sign in

More articles by Cassio Almeron

  • Entity Framework: In Memory for Automated Tests.

    I’ve been studying EF Core, specifically following the course “Entity Framework Core - A Full Tour” by Trevoir Williams…

    9 Comments
  • TDD Over Databases

    I don’t know how many systems are still working with its core implemented inside the database, but I believe it is not…

    6 Comments
  • Strategy Pattern for TDD

    Well, applying TDD over simple and isolated methods is something quite simple. This example below is a simple…

    9 Comments
  • Reuse query statements in SQL with Cross Apply

    Have you ever had a situation where you needed to repeat code in an SQL command? I had many times, and I used to copy…

    14 Comments
  • System.Lazy in C#

    During a work task, I was asked to manage the loading of an object just once in its first usage. It is something that I…

    8 Comments
  • The Misconception of Forgiveness in Portuguese.

    A few times ago, I read the book “The 6 Phase of Meditation Method” by Vishen Lakhiani, which abords the concepts and…

    1 Comment

Insights from the community

Others also viewed

Explore topics