Tuesday 4 November 2014

Improving the testability of complex MVP view implementations

When implementing a view in a separated presentation design pattern, there’s value in keeping the implementation as simple as possible. Speaking from a Windows Forms perspective, concrete views are hard to test in an automated way. The testing of how the actual view performs is often performed manually. Therefore, keeping the code in the concrete view as simple as possible and leaving the more complex logic with the presenter, which can be covered with automated tests, appears to be a sensible approach.

For example, I recently developed a presenter that needed to create a visual representation of a hierarchy. While I was sure that my concrete view would use a TreeView, I like to keep my view interfaces abstract and ‘agnostic’ of the actual controls that I might use. Therefore, a simplistic version of my view interface looks like this:

interface INavigationView
{
   void AddParent(int id, string description);
   void AddChild(int id, string description, int parentId);
}


This view should be abstract enough that it can be implemented using any type of control or combination of controls.

The implementation of AddParent() is simple enough:

public void AddParent(int id, string description)
{
   var rootNode = TreeView.Nodes[RootNodeName];

   var parentNode = 
      rootNode.Nodes.Add(key: id.ToString(), text: description);

   parentNode.Tag = NodeType.Parent;
}

Simple enough, I’d argue, to not warrant automated test coverage. If this code doesn’t work, it’s going to be obvious to the end user pretty quickly. However, when we get to the implementation of the AddChild() method, it gets a little more complex:

public void AddChild(int id, string description, int parentId)
{
   var potentialParents = nodes.Find(key, searchAllChildren:true);

   var potentialParentsCount = potentialParents.Count();

   TreeNode actualParent = null;

   if (potentialParentsCount == 1)
   {
      actualParent = potentialParents[0];
   }
   else if (potentialParentsCount > 1)
   {
      actualParent = 
         potentialParents.SingleOrDefault(n => n.Tag.Equals(tag));
   }

   if (actualParent != null)
   {
      var childNode = 
        actualParent.Nodes.Add(key:id.ToString(), text:description);

       childNode.Tag = NodeType.Child;
   }
}

There’s more complex logic here that I'm uncomfortable leaving to a manual test. Furthermore, the logic is very much related to the Windows Forms TreeView control, so it’s not logic I can promote to the presenter and test there. So, assuming that I cannot cover this view implementation with automated tests, I can re-factor this code into its own class, making it easier to test:

class TreeNodeFinder
{
   public static TreeNode Find(
      TreeNodeCollection nodes, 
      string key, 
      object tag)
   {
      var potentialParents = 
         nodes.Find(key, searchAllChildren: true);

      var potentialParentsCount = potentialParents.Count();

      TreeNode actualParent = null;

      if (potentialParentsCount == 1) 
      {
         actualParent = potentialParents[0];
      }
      else if (potentialParentsCount > 1) 
      {
         actualParent = 
           potentialParents.SingleOrDefault(n => n.Tag.Equals(tag));
      }

      return actualParent;
   }
}

We now have a class that we can comfortably cover with automated tests. We can then integrate this class into our view, reducing the amount of code not covered by automated tests and, arguably, making the intent of the code clearer:


public void AddChild(int id, string description, int parentId)
{
   var parent = 
      TreeNodeFinder.Find(
         TreeView.Nodes, 
         id.ToString(), 
         NodeType.Parent);

   if (parent != null)
   {
      var childNode = 
         parent.Nodes.Add(key: id.ToString(), text: description);
      
      childNode.Tag = NodeType.Child;
   }
}

No comments:

Post a Comment