2

Code Testability: A Case Study

 3 years ago
source link: https://blog.ndepend.com/code-testability-a-case-study/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Code Testability: A 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.
         public bool IsSupportedByTheCurrentOperatingSystem {
#if DEBUG
            [UncoverableByTest] // Coz depend on OS!
#endif
               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.

      public bool IsSupportedByTheCurrentOperatingSystem {
             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:

      [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.

My dad being an early programmer in the 70's, I have been fortunate to switch from playing with Lego, to program my own micro-games, when I was still a kid. Since then I never stop programming.

I graduated in Mathematics and Software engineering. After a decade of C++ programming and consultancy, I got interested in the brand new .NET platform in 2002. I had the chance to write the best-seller book (in French) on .NET and C#, published by O'Reilly (> 15.000 copies) and also did manage some academic and professional courses on the platform and C#.

Over the years, I gained a passion for understanding structure and evolution of large complex real-world applications, and for talking with talented developers behind it. As a consequence, I got interested in static code analysis and started the project NDepend.

Today, with more than 8.000 client companies, including many of the Fortune 500 ones, NDepend offers deeper insight and understanding about their code bases to a wide range of professional users around the world.

I live with my wife and our twin babies Léna and Paul, in the beautiful island of Mauritius in the Indian Ocean.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK