NDepend Blog

Improve your .NET code quality with NDepend

Code Testability: A Case Study

September 1, 2021 3 minutes read

Code-Testability-Case-Study
The next NDepend version will run on Linux and MacOS and thus, the NDepend.Path library now has to support Linux style path. During this move we stumbled on this untestable property getter (below). It determines that a Linux style path is not supported on Windows and that a drive letter or Windows UNC path is not supported on Linux and MacOS.

This method is untestable because its logic depends on NdpOperatingSystem.Kind which returns an OSPlatform object. Notice the usage of the attribute UncoverableByTest that lets code reviewers and tools like NDepend knows that this method cannot be 100% covered by test. This is useful because in our dev shop 100% coverage by tests is part of our definition of done.

The testable version of the code with tests

However this logic can be 100% tested. To make it testable a new method must be created to abstract away the call to NdpOperatingSystem.Kind. The trick is to separate the unpredictable environment call with its processing. Here the environment call is NdpOperatingSystem.Kind but it can be DateTime.Now or Environement.UserName.

Here is the testable version.

Notice the usage of the attribute [CannotDecreaseVisibility]. The introduced method should be declared as private because it is used only in its class. However to make it callable from test we make it internal and tag its parent assembly with InternalsVisibleToAttributes. Some NDepend rules that check for optimal visibility won’t warn here thanks to the usage of [CannotDecreaseVisibility]. This is also a good way to embed in code the intention of making this method fully testable.

Here are the tests:

This trick vs. Dependency Injection (DI)

The rule of thumb when it comes to code testability is to inject code. With Dependency Injection environment calls can be hidden behind an interface that can be mocked at test time.

For simple situations like this one it is preferable to just embed the logic into a static & pure method that takes all the state it needs as arguments. This is an application of the KISS principle, Keep It Simple Stupid!

On a design side note, IsSupportedByTheCurrentOperatingSystem is well designed as a property getter because during the same execution, it always return the same value. A counter-example of that principle is DateTime.Now that should be a method because when calling the member twice in succession produces different results (see a discussion on that point here).

Conclusion

This trick might look awkward since now we have two methods instead of one. Isn’t the design more complex? Actually code testability is an essential part of good design, if not the number one design property. If it’s hard to test or even untestable as it was here, it is not well designed.

Unlike most SOLID principles that are sometime considered as Cargo Kult principles,  the testability property is something concrete. Code that is easy to test is necessarily more maintainable and thus better designed, than hard to test code.

 

Comments:

Comments are closed.