Verdict
Yes
Test an internal class that holds logic with its own unit tests, and give the test project access to it instead of making the class public.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityeventually
- Reversibilitytrivial
- Test costtrivial
Give an internal class that holds logic its own unit tests. The typical case is a C# or Kotlin internal class, or a Java package-private class, that calculates or decides something behind the public classes of a service. Blast radius is users and Change frequency is regularly, because a wrong result reaches customers and the logic changes about once a month. Detectability is eventually, because the public tests reach only a few of the class's cases, and a wrong branch returns a plausible value. Reversibility is trivial because a redeploy removes the bug, Test cost is trivial because the test calls the class without reflection, and rule R11 gives Test.
| When | Decision | Why |
|---|---|---|
| The internal class calculates the prices that customers are charged | Test mandatory: direct unit tests with boundary amounts, plus a test through the public API | Blast radius rises to money and Reversibility to costly |
| The internal class decides whether a user may read a record | Test mandatory, including every case where access is denied | Blast radius rises to safety-or-legal and Detectability to never, because a missing check shows the record without an error |
| The internal class only forwards one value, or values of different types, to another class, and the public tests run it with data that tells the target method apart from its siblings | Do not test the internal class directly; the public tests cover it | Detectability moves to immediately, because the compiler or a public test fails |
| The internal class copies or forwards several values of one type, such as a start date and an end date | Test: one unit test with a distinct value in each field | Detectability stays eventually, because a swapped value is plausible and of the right type |
| The tests of the public classes already run every branch of a small internal class with data that gives each branch a different result | Do not add direct tests of the internal class; keep the public tests | Detectability moves to immediately, because a broken branch fails a public test in CI |
| The internal class feeds an internal dashboard and changes a few times a year | Test minimally: one unit test of the main calculation | Blast radius falls to internal and Change frequency to rarely |
What breaks if you don't test
A public class often combines several internal classes, and its tests reach every internal class with the same few inputs. An internal calendar that skips weekends can pass every public test that orders on a Tuesday, then pick a Saturday for a Friday evening order. Customers see a delivery date the warehouse cannot meet.
What you lose if you over-test
Direct tests of every internal class pin the internal structure. Tests of classes that only forward one value, or values of different types, fail when you merge or split classes, although no caller sees a change.
How to test
Write unit tests against the methods of each internal class that holds logic. Cover boundary values, empty input and the branches the public tests miss. Give the test project access through the language: the InternalsVisibleTo attribute in C#, a test in the same package in Java, and the test source set in Kotlin, which can access internal declarations of main. Keep at least one public-level test that runs through the wiring between internal classes.
When the answer changes
- The internal class calculates an amount that customers pay.
- The internal class only forwards one value, or values of different types, and the public tests run it with data that tells the target method apart from its siblings.
- The tests of the public classes run every branch of the internal class with data that tells the results apart.
Real incident + Code example
The Friday order that shipped on Saturday
On a checkout service I worked on, a public DeliveryEstimator used an internal DispatchCalendar for the 14:00 cutoff, weekends and bank holidays. The estimator tests placed orders on weekday mornings, and the calendar had no tests. A change for bank holidays reordered two checks, so a Friday order placed after the cutoff got a Saturday dispatch date, when the warehouse was closed. Product pages promised deliveries a day early for three weeks, until support linked the complaints about late parcels. The fix was a test of the calendar itself, which one attribute made possible:
// Checkout/AssemblyInfo.cs
[assembly: InternalsVisibleTo("Checkout.Tests")]
// Checkout.Tests/DispatchCalendarTests.cs
public class DispatchCalendarTests
{
[Fact]
public void FridayOrderAfterCutoffDispatchesOnMonday()
{
var calendar = new DispatchCalendar(new TimeOnly(14, 0), Array.Empty<DateOnly>());
var placed = new DateTime(2026, 9, 18, 15, 30, 0); // a Friday, after 14:00
Assert.Equal(new DateOnly(2026, 9, 21), calendar.DispatchDate(placed));
}
}
Related questions
FAQ
- Should internal classes be unit tested?
Yes, give an internal class that holds logic its own unit tests, because the tests of the public classes seldom reach all of its cases. An internal class that only forwards one value, or values of different types, needs no tests of its own when the public tests run it with data that tells the target method apart from its siblings.
- How do I test internal classes in C#?
Add
[assembly: InternalsVisibleTo("MyProject.Tests")]to the assembly under test, and the test project can call its internal classes like public ones. If the assembly has a strong name, the InternalsVisibleTo attribute must also carry the full public key of the test assembly.- Should I make an internal class public to test it?
No, other assemblies can depend on a public class, so later changes can break their code. Give the test project access:
InternalsVisibleToin C#, a test in the same package in Java, or thetestsource set in Kotlin.- Should I test package-private classes in Java?
Yes, test a package-private class that holds logic from a test class in the same package, because Java lets that test call the class without reflection. Put the test under
src/test/javawith the package name of the class.