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 |
Figure 1 – Analysis Chart Control
| public class LongTermProfitAnalysisTest extends MDIToolTestBase |
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.
- The logic depends on the view and view life cycle events like CreationComplete.
- The logic operates on complicated internal data structures
- 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.
Figure 3 – Logical Components
- 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.
- 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.
- 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:
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.
Post a Comment