[C#] Internal classes and generic unit-testing baseclasses

This is another blog post in the series of “Things that gave me plenty of grief, but eventually got solved”.

I was working on a component that utilizes the Azure DevOps RESTFul API to create a report over work done, and figured it’d be so useful that I’d probably reuse it for other projects that I’m working on.  Outwards, the solution exposes ONE Service: public class ReportService but internally it used a whole bunch of support classes that I didn’t want to muddle up the namespace with so I changed their access from public to internal to hide them from the end-users view.

INTERNAL CLASS

The internal class  modifier changes the visibility of the class to appear visible only within the assembly in which it was defined. Thus, if you’re working in a solution where your main UI is in the executable  SomeProgram.exe, and your business logic is all contained within the library SomeProgram.BusinessRules.dll then you’d declare most classes internal  so that when you’re coding in SomeProgram.exe, only the public classes are visible to you. This reduces clutter for the end user and is perfect if you want to create  a NuGet package for example.

UNIT-TESTING SMARTER

If you’ve read any of my past blog posts, you’ll know I often tend to create a base-class for unit testing that does away with all my mocking needs. In it’s most recent form, it looks something like the following:

public class TestsFor<TInstance> where TInstance : class { /* implementation */ }

This allows me to create testclasses really fast, and ensures I can test each class in isolation, having all of it’s constructor dependencies mocked out:

public class ReportServiceTests : TestsFor<ReportService>
{
  [Fact]
  public async Task CreateReport_WhenCalled_LogsTheCall()
  {
   // Act
   await Instance.CreateReport();

   // Assert 
   GetMockFor<ILogger>().Verify(logger => logger.Enter(Instance, nameof(Instance.CreateReport)), Times.Once());
}

In the above unit test, Instance is created by the base class, and is an instance of  the ReportService class with all dependencies (like the ILogger) mocked out.  I often use xUnit for my unit-testing needs, as I’ve done above.

OY VEY INTERNALS!

The problem with this design is that it does not work for internal classes. Consider the following assemblies:

  • ReportService.Business (Contains internal class ExceptionHandler)
  • ReportService.Business.Tests  (Contains all unit-tests for objects declared in ReportService.Business, among them public class ExceptionHandlerTests : TestsFor<ExceptionHandler> )
  • UnitTestitng.Tools (Contains our base-class TestsFor<T> for unit-testing)

Like any good developer, you’ll know that in order to make a class that is marked internal visible to another assembly, you can add the assembly instruction [assembly: InternalsVisibleTo(“”)] and then life will be good:

(snippet from ReportService.Business.AssemblyInfo.cs)

[assembly: InternalsVisibleTo("ReportService.Business.UnitTests")]
[assembly: InternalsVisibleTo("ReportService.UnitTestingTools")]

This however does NOT work for us! The declaration  public class ExceptionHandlerTests : TestsFor<ExceptionHandler> attempts to create a public class using the internal class ExceptionHandler and boom! You now have yourself an inconsistent accessibility error that does not compile, and certainly does not allow you to run any unit tests! Do you feel my panic?

THE SOLUTION: WRAP IT

So the problem is that we cannot derive an internal class into a public class, because that would expose the internal methods and make them available publicly. The solution is to wrap the internal class into a private class inside your unit test class so that it does not conflict. The keyword private is LESS accessible than internal and thus poses no threat to inconsistency rules.

public class ExceptionHandlerTests 
{
  private InternalTestClass test = new InternalTestClass();

  private class InternalTestClass : TestsFor<ExceptionHandler>
  {
  }

  [Fact]
  public void RunSyncronously_CalledWithNull_LogsWarningMessage()
  {
    // Arrange
    Func<Task> nullFunc = null;

    // Act
    test.Instance.RunSyncronously(nullFunc);

    // Assert
    test.GetMockFor<ILogger>().Verify(logger => logger.LogWarning(It.IsAny<string>()), Times.Once());
  }
}

The re-write did not take much time at all, and allowed me to leverage the full power of my unit-testing base-class without having to sacrifice the internal keyword.

 

 

 

About digitaldias

Software Engineer at Microsoft during the day, photographer, videographer and gamer in the evening. Also a father of 3. Pedro has a strong passion for technology, and gladly shares his findings with enthusiasm.

View all posts by digitaldias →

Leave a Reply

Your email address will not be published. Required fields are marked *