1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public bool IsSupportedByTheCurrentOperatingSystem { #if DEBUG [UncoverableByTest] // Coz depend on OS! #endif get { bool isWindowsOS = NdpOperatingSystem.Kind.IsWindows(); if (isWindowsOS && m_Kind.EqualsAny(AbsolutePathKind.DriveLetter, AbsolutePathKind.UNC)) { return true; } if (!isWindowsOS && m_Kind == AbsolutePathKind.LinuxStyle) { return true; } return false; } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public bool IsSupportedByTheCurrentOperatingSystem { get { return IsSupportedByTheCurrentOperatingSystemPriv( m_Kind, NdpOperatingSystem.Kind); } } #if DEBUG [CannotDecreaseVisibility] // Coz used by test for 100% coverage! #endif internal static bool IsSupportedByTheCurrentOperatingSystemPriv( AbsolutePathKind kind, OSPlatform osPlatform) { if (osPlatform.IsWindows() && kind.EqualsAny(AbsolutePathKind.DriveLetter, AbsolutePathKind.UNC)) { return true; } if (osPlatform.IsLinuxOrMacOS() && kind == AbsolutePathKind.LinuxStyle) { return true; } return false; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
[TestCase(AbsolutePathKind.LinuxStyle, NdpOperatingSystem.s_LinuxStr, true)] [TestCase(AbsolutePathKind.UNC, NdpOperatingSystem.s_LinuxStr, false)] [TestCase(AbsolutePathKind.DriveLetter, NdpOperatingSystem.s_LinuxStr, false)] [TestCase(AbsolutePathKind.LinuxStyle, NdpOperatingSystem.s_MacOSStr, true)] [TestCase(AbsolutePathKind.UNC, NdpOperatingSystem.s_MacOSStr, false)] [TestCase(AbsolutePathKind.DriveLetter, NdpOperatingSystem.s_MacOSStr, false)] [TestCase(AbsolutePathKind.LinuxStyle, NdpOperatingSystem.s_WindowsStr, false)] [TestCase(AbsolutePathKind.UNC, NdpOperatingSystem.s_WindowsStr, true)] [TestCase(AbsolutePathKind.DriveLetter, NdpOperatingSystem.s_WindowsStr, true)] public void Test_IsSupportedByTheCurrentOperatingSystem( AbsolutePathKind kind, string osPlatformStr, bool supported) { var osPlatform = NdpOperatingSystem.GetOSFromString(osPlatformStr); bool b = PathHelpers.IsSupportedByTheCurrentOperatingSystemPriv(kind, osPlatform); Assert.IsTrue(b == supported); } [Test] public void Test_IsSupportedByTheCurrentOperatingSystem() { Assert.IsTrue( @"/user/share".ToAbsoluteDirectoryPath().IsSupportedByTheCurrentOperatingSystem == NdpOperatingSystem.Kind.IsLinuxOrMacOS()); Assert.IsTrue( @"C:\Dir".ToAbsoluteDirectoryPath().IsSupportedByTheCurrentOperatingSystem == NdpOperatingSystem.Kind.IsWindows()); } |
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.
awesome