Feb 1, 2012

Unit Tests as a Measure of Code Cleanliness

 

    I recently attended the 2011 Adobe MAX conference and sat in on the Unit Testing Adobe Flex session by Michael Labriola of Digital Primates.  I expected the session to be a repeat of what I already know and strongly believe.  Unit tests simplify testing, document your code, improve maintainability, blah blah blah.  The aspect of the lecture that I found most fascinating was on writing clean code and using unit tests as a means to verify code cleanliness. 

    My training regarding unit tests has been pretty informal.  Through experience I’ve found that when I write automated tests my bug count goes way down, and when I do have a bug I have a great starting point to isolate and fix issues.  I’ve always understood the difference between unit, integration, and functional testing but never much cared because all 3 have the benefits mentioned above.  The simple discovery, however, that being able to unit test your code in the strictest sense is a very good measurement as to the cleanliness of your code was pretty eye-opening.  If your code base is only testable at the integration level, your code is probably too tightly coupled and does not have good encapsulation.

Example

    I am currently working on a very large project that charts, maps, analyzes, and performs a million other visualizations and calculations on complex data.  It follows a simple MVC architecture such that I am able to isolate specific view behavior into 1 or more specific control classes.  It generally results in pretty clean code, and I haven’t had too much trouble maintaining it.  I write automated “flexunit” test cases that verify things work properly, and give me a decent starting place when I do run into a bug.  That said, however, I still have areas of the code that cause me problems.  These areas have been refactored over time, and improved to some degree, but they still give me more pain than I’d like.

    After the Michael Labriola lecture I decided to take a look at my code and automated tests from a strict policy that automated tests must be “unit” tests, not “functional” or “integration” tests.  Michael’s presentation defined a unit test as

  • testing a single object
  • should not be affected by other objects
  • does not depend on global state
  • does not depend on server, lifecycle, display lists, or asynchronous events (these are integration level testing).
  • When a unit test fails, the source of the error should be immediately obvious

    Take a look at one of my more troublesome view controls, and the corresponding automated test.  The code below is the control logic for a view that displays a line chart representing value data.  It is possible for the user to add data to the chart for display or add filters that reduce the data being displayed.

public class LongTermProfitAnalysisToolCtrl extends DataAndFilterToolWindowCtrl
{
    //...
    protected override function onCreationCompleteView(event:ViewLifeCycleEvent) :
void
    {
        //Set default filter and analysis calculation values
    }
       
    //Private methods that listen to data changes
       
    //Private methods that listen to user input changes
       
    //Private methods that calculate Annual income analysis info based on data
    // and user input
       
    //Private methods that calculate monthly income analysis.
       
    //Private methods that calculate ROI analysis info based on data and
    //user input
       
    //Private methods that would update chart line series data based on
    //analysis calculations
       
    //Private methods that create data tips on the chart
}

Figure 1 – Analysis Chart Control

public class LongTermProfitAnalysisTest extends MDIToolTestBase
{   
    Before( async, ui)]   
    public override function setUp():void   
    {        
        //register views and controls with MVC framework           
       
        //Create tool, and proceed on CreationComplete event   
    }    
   
    /**
    *  Test adds an item to be analyzed, and verifies the analysis output   
    *  is correct.   
    **/   
    [Test]   
    public function testAddItemToAnalyze():void   
    {       
        //Create test data       
        //Add test data to tool       
        /* Look at chart data and verify the values are what you are expecting with regard to ROI, Annual & Monthly income.  And when it doesn’t, raise your fist into the air and curse the developer (me) whowrote this crappy code and the fact that this unit test doesn’t help you narrow down the problem.  */ 
    }    
   
    /**   
    *  Test when a filter object is added, the resulting data output   
    *  is correct.   
    **/  
    [Test]   
    public function testFilters():void   
    {       
        //Create test data       
        //Add test data to tool       
        //Add a filter       
        /* Look at chart data and verify the values are what you are expecting with regard to ROI, Annual & Monthly income.  And when it doesn’t, raise your fist into the air and curse the developer (me) whowrote this crappy code and the fact that this test doesn’t help you narrow down the problem.  */  
    }
}  

Figure 2 – Analysis Chart Control Test

Flaws in Unit Testability & The Underlying Problems

    Looking at the tests above, it is easy to see that these are integration tests, not unit tests.   In order to even begin testing I must create a visualization, and wait for creation complete.  I then have to create a rather complicated set of interrelated test data, and at the end of it all I can only determine correctness.  The tests provide me no insight as to what could be incorrect. 

    So it looks like my code is not unit testable as written, and sure enough when you look at the code under test you can see why.  The control class has 10 different responsibilities.  It inherits from a large class hierarchy that performs a great deal of work that has nothing to do with what I am trying to test.  The encapsulation is terrible and a great deal of test data must first be setup in order to run a simple test.

Refactoring Approach

    One of the major difficulties with refactoring is that it is often difficult to figure out how to cleanly separate your code.  For many of my complex tools I had broken the code down cleanly and so it wasn’t apparent that the example above was following a different and unclean pattern.  I didn’t even realize that my approach was haphazard and less than optimal until I hit a wall.  Fortunately as I learned in the lecture, unit tests not only raise a red flag, they can guide you in how to re-factor your code.

Step 1:  Identify each method / capability that should be unit tested

    Take a look at each method / capability and identify which are worth unit testing.  In my case it is all the calculation functions, as well as the ability to update the visualized chart data when input data changes.

Step 2:  Identify why code can’t be unit tested

    In my case there are a dozen reasons this code can’t be unit tested in the strictest sense, but here are some of the big problems.

  1. The logic depends on the view and view life cycle events like CreationComplete.
  2. The logic operates on complicated internal data structures
  3. All the calculations are private, the only thing that is public is the output.

Step 3:  Group high level capabilities into units of single responsibility

    Analyzing the code, and understanding what capabilities I want to test, the code can be broken down into 3 logical components.  Note:  At this point these are “logical” components, and may translate into 1 or more classes.

LogicalSubsystems

Figure 3 – Logical Components

  1. ViewController – The view controller listens to user input, and triggers actions with the Analyzer_Calculator and the CalculationToLineSeriesTranslator.  This logical component is now trivial, and honestly can survive with only integration testing.
  2. Analyzer_Calculator – The analyzer / calculator logical component will need to perform mathematical calculations like “return on investment” for a given piece of data.  This logical component doesn’t depend on anything and can be written as calculation utility methods which will be very easy to unit test.
  3. CalculationToLineSeriesTranslator – The translator converts data and calculations into line series data that the chart view can draw.  This logical component will not need to depend upon anything and can be unit tested very easily.  Given some input data values, verify the output is lines series data with values that match the input.

Step 4:  Refactor existing logic into new components

    At this point you’ve already solved the hard problems.  Refining down to individual classes is fairly trivial.  For example, my “Analyzer_Calculator” logical component was implemented as several specific calculator classes that were really just groupings of related utility calculation methods.  Each was very easy to test and verify.

Conclusion

    Learning to use unit tests in the strictest sense of “unit” have been invaluable to me in my own development.  It is often very difficult to tell the difference between code that is “good enough” and code that is “good”, and unit testability provides a pretty good threshold measurement.  No rule is infallible, but so far in every case where I have re-factored my code around strict unit testability I’ve seen a dramatic increase in code quality. 

1 comment:

Desentupidora | Plumber said...

Hey! Eu só gostaria de dar uma enorme polegares para cima para os dados grande que você poderia ter aqui neste post. Eu posso estar chegando novamente ao seu blog para extra em breve.