Thursday, May 30, 2013

Some advanced code testing

For those who don't want or don't have the time to read thick books or framework testing APIs, I have collected a few topics that can help improve your unit testing but might not always be obvious. Note that they're not purely restricted to C# even though the examples are.

Mighty testing frameworks

There are some mighty mocking and unit testing frameworks out there with impressing features like mocking dependent-on static methods, testing private members etc. Although in some cases these are vital, e.g. when writing tests for legacy code, they might lead you to write your code with lower quality taking less care about class design, dependencies etc. What's better: code quality or a new (and possibly costly) dependency on a mighty framework? Code quality does not depend on the language you're programming in, it depends mainly on you as a developer!

Use delegates for dependency to static methods

C# delegates can work like functions in languages like Python or Go where they are first class citizens. This is a good thing, e.g. in the following case:
 public void Init()  
 {  
      var content = File.ReadAllText(this.Path);  
      ...  
 }  
We have a dependency here making testing somewhat difficult. Whereas in some situations it's good pratice to wrap call to static methods into an instance, here we can get along without it:
 public void Init()  
 {  
      this.Init(File.ReadAllText);  
 }  
 internal void Init(ReadAllText readAllText)  
 {  
      var content = readAllText(this.Path);  
      ...  
 }  
 internal delegate string ReadAllText(string path);  

Dependencies to the surface

Somewhat related to the last topic on delegates is the the following: Often we programm quickly and only afterwards discover that we have dependencies, especially to built-ins like System.IO.FileInfo. Now, built-ins can have bugs, too. And a dependency on them should better be injected, e.g. for testability or for extendability. We can avoid missing those dependencies by not using global imports, by deleting any using statement at the beginning of our files and using full namespaces. What is the difference between
 using System.IO;  
 using System.Xml;  
 ...  
 internal void Init()  
 {  
      var file = new FileInfo(this.Path);  
      var dir = new DirectoryInfo(this.DirPath);  
      var xdoc = new XmlDocument();  
      ...  
 }  
and
 internal void Init()  
 {  
      var file = new System.IO.FileInfo(this.Path);  
      var dir = new System.IO.DirectoryInfo(this.DirPath);  
      var xdoc = new System.Xml.XmlDocument();  
      ...  
 }  
The difference is that in the second case you're getting so tired of typing the namespaces that you will want to do something about it. For the moment you cannot use global imports, so you will need to refactor to dependency injection, interface extraction etc.

The protected antipattern

There is this rule that production code and test code should not be mixed. In .NET we choose to create seperate test projects and only test against the public interface, letting private members become internal to be testable if necessary and sensible to do so.
Another possibility is to set protected instead. Then in our test project we just inherit and overwrite.
In my experience though, this second approach has at least two downsides that the use of internal doesn't have:

  • the access modifiers totally loose their sense because we cannot control what's done with protected members.
  • the inherited class will be tested and in a more complicated setup in the end we might loose track. Did we really test our code or the test code?

Any vs. Some

In order for our test cases to serve as documentation we need method and variable names communicating intention:
 public void TestCaseConstructorSetsProperties()  
 {  
      var to = new TestObject(anyParameter());  
      AssertThatPropertiesAreSet(to);  
 }  
My personal gusto is only to use the prefix any if null is permitted, too. So if - following common practice - the constructor checks for null argument, this will be another test case. We can opt for using anyNonNullParameter() or simply:
 public void TestCaseConstructorSetsProperties()  
 {  
      var to = new TestObject(someParameter());  
      AssertThatPropertiesAreSet(to);  
 }  

Advanced setup and teardown - cleanup files example

Have a look at the following code:
 public void TestCaseUsingFileSystem()  
 {  
      var file = createTestFile();  
      var to = new TestObject();  
      to.doSomething(file);  
      AssertThatSomethingHoldsOn(to);  
      file.Delete();  
 }  
The problem here is that the file probably won't be deleted if the assertion fails, making this test case fragile. Surely, you can think of other objects that might need proper tear down even if the test fails in order to assure correctness of the test fixture. A common solution of this problem is to introduce some class variable serving as trash and using a shared teardown. Mind also the file creation method which simply could have been called createTestFile() as before:
 public void TestCaseUsingFileSystem()  
 {  
      var file = createAndRegisterForCleanupTestFile();  
      var to = new TestObject();  
      to.doSomething(file);  
      AssertThatSomethingHoldsOn(to);  
 }  
 public void TearDown()  
 {  
      foreach(var item in this.trash)  
      {  
           try  
           {  
                var file = item as File;  
                if(file != null) file.Delete();  
                ...  
           }catch(Exception e){  
                reportToTestRunner(e.Message);  
           }  
      }  
 }  
 private File createAndRegisterForCleanupTestFile()  
 {  
      var file = createTestFile();  
      this.trash.Add(file);  
      return file;  
 }       

Event checking

You should always check if events are raised, too! An easy pattern for doing so is this:
 public void TestCaseSomeMethodRaisesEvent()  
 {  
      var eventHasBeenRaised = false;  
      testObject.SomeEventHandler = (sender, args) => eventHasBeenRaised = true;  
      testObject.SomeMethod();  
      AssertThat(eventHasBeenRaised);  
 }  

No comments:

Post a Comment