Oh, the Pain!
For a long time, unit testing non-public methods in C# has been a pain in the back side. In order to test a non-public method (most likely private methods) you have a number of choices each with an undesired effect:
- The easiest way out is to make the methods public. However, this breaks all the encapsulations in your application and can be potentially unsafe because in most cases these methods were not exposed publicly for a reason, e.g. a method which can leave your application in a bad state if used incorrectly.
- If making the methods public is a step too far, you can make these methods protected instead and have the unit test class extend this class to gain access to the protected methods. Still, this breaks the encapsulation in your application somewhat, and having a unit test class extend a business logic class ‘smells’. And once again, it can be unsafe as others can also extend your class to gain unintended access to these methods, the same reason why you would often want to seal the LAST type in an inheritance hierarchy – to mark and protect the boundaries of your application.
- The last and the most advanced technique is to use reflection to invoke these methods. The upside is that you won’t have to break the encapsulation of your application just to make them testable, the downside is that you pay for this in maintainability and complexity. What it means is that your test class now needs to have intimate knowledge of the class it is testing and because methods names are spelled out in string literals, renaming the non-public method becomes a more costly operation because you need to change all the reflection code used to invoke the method too. Personally this is my least favourite approach as the frequency of change and the number of unit tests in any sizable project can make the task of maintaining them rather unenviable (especially if it’s your job to do so!).
There’s a better way
All you need to do is add one line of code to the AssemblyInfo.cs file of the assembly (say, MyApplicatoin) you want to test to allow your unit test assembly (let’s call it MyApplication.Test) to see all the types/methods that have been marked with the internal access modifier:
You will also need to change the access modifier on any methods (in MyApplication) you wish to test to internal for you to be able to see it in MyApplication.Test.
Doing so, however, doesn’t stop you from sealing your type (point 2 above) and doesn’t break all the encapsulation of your application (methods are still inaccessible from external assemblies besides the unit test assembly) although it still breaks the encapsulation of your type because any private methods you wish to test have to be made internal instead and therefore accessible from outside the type.
This is still not ideal, but compared to the alternatives, is still the one that I’m most willing to live with.
Before you jump in with both feet and start testing all the private/internal methods, there’s a couple of questions you should always ask yourself first:
- The most important question of them all is whether or not these methods are even worth testing!
- If they are worth testing, could they be moved into a separate class?
- Could you cover all the test cases by making more calls to the public methods that call the private methods?
I’m an AWS Serverless Hero and the author of Production-Ready Serverless. I have run production workload at scale in AWS for nearly 10 years and I have been an architect or principal engineer with a variety of industries ranging from banking, e-commerce, sports streaming to mobile gaming. I currently work as an independent consultant focused on AWS and serverless.
Here is a complete list of all my posts on serverless and AWS Lambda. In the meantime, here are a few of my most popular blog posts.
- Lambda optimization tip – enable HTTP keep-alive
- You are thinking about serverless costs all wrong
- Many faced threats to Serverless security
- We can do better than percentile latencies
- I’m afraid you’re thinking about AWS Lambda cold starts all wrong
- Yubl’s road to Serverless
- AWS Lambda – should you have few monolithic functions or many single-purposed functions?
- AWS Lambda – compare coldstart time with different languages, memory and code sizes
- Guys, we’re doing pagination wrong