In this lesson, you will learn how to verify the logic inside private methods and internal members without breaking the encapsulation that keeps your codebase clean.
The Testing Paradox
When you feel the need to test a private method, it is often a "code smell" suggesting that the method contains complex logic that should exist as its own class. However, in legacy systems or high-performance scenarios, you might need visibility into internal code. Testing private methods directly is a violation of the "Test Behavior, Not Implementation" principle—if you rename a private method, your tests shouldn't break.
Exposing Internals via InternalsVisibleTo
If you have logic that is truly internal (scoped to the assembly), you can grant your test project access to it without making it public to the rest of the world. By adding the following to your AssemblyInfo.cs or project file, you allow your test suite to "see" your internal members:
[assembly: InternalsVisibleTo("MyProject.Tests")]
By changing a method from private to internal, you maintain architectural integrity—the method remains hidden from consumers of your production NuGet package while becoming fully testable for your unit test assembly.
The Strategy: Extract, Don’t Expose
Before reaching for InternalsVisibleTo or complex reflection hacks, consider the "Extract Class" strategy. If a private method is complex enough to require dedicated tests, it likely violates the Single Responsibility Principle.
- Move the logic: Extract that private method into a new, internal service class.
- Inject the dependency: Inject this new class into your original service.
- Test the new class: You can now test the complex logic as a public method on the new class.
This approach keeps your code modular and your test suite expressive. Instead of testing implementation details, you are testing a distinct, focused piece of behavior. If you find yourself struggling to test a private method, take it as a signal that the code is asking to be moved into its own unit.
Key Takeaway: Prefer extracting complex logic into its own testable class; use InternalsVisibleTo only when you need to maintain encapsulation while sharing specific logic across the assembly boundary.