Thursday, September 3, 2009

"As simply as possible"

Yesterday, I held an internal TDD Introduction workshop for about 10 developers.

I had no slides, just winging it using VS2008 on a projector. And using the whiteboard.

The workshop was in two parts.
1. The common calculator that eventually become string based requiring a tokenizer. This is to push the Red-Green-Refactor envelope. I know : pretty boring, but you want to put the emphasis on the tight cycle of TDD.

2. A case with external dependencies.
The specific use case was a HVAC controller. It starts off as a simple thermostat to control a cooling fan, but evolves to a full fledged HVAC controller.
People discover that the API of a thermostat is principally different than the HVAC controller. For example, "Desired temperature" can be a single number in a thermostat, whereas the HVAC controller needs a desired temperature with an upper and lower limit. The pattern geeks may argue that the HVAC controller is actually two thermostats working in opposites (one for "cold", one for "heat"), which certainly makes for some interesting programming.

The three immediate takeaway benefits are:
* TDD forces you to think about your interface. And working in pairs produces some fruitful debate at a very early stage. Paradoxically, people suddenly find themselves unable to express their use cases against their own API (and then they start blaming me).

* They discover that there is a "secret" dependency on time. One dependency driving requirement is that the airfan should run for two minutes after the furnace has been switched off. Hopefully they'll muck around with DateTime static helpers before realizing that time must be controlled from the tests.

* The manual mocks versus isolation frameworks (Moq). The initial test cases make for slim manual fakes, but the Moq syntax is usually heartly received.

We only had half a day, which went by in the blink of an eye, but I think that we ran into many of the typical subjects.
There certainly is stuff that is hard to get even in the simple world of TDD. We met some speed bumps when we crossed the following:

Problem 1: Lambda expressions.
I have opted for our group to go for Moq as the isolation framework of choice. This is because of the crisp syntax and a lot of sense and intellisense. The most noted objection against Moq is that it makes heavy use of Lambda expressions and therefore is only available in the .NET 3.5. However, that is no concern for us.
We had some MFC programmers in the workshop. Even seasoned and capable programmers are visibly in pain when they try to wrap their heads around Lambda expressions. I wanted to showcase the evolution of bamed method -> Anonymous method -> Lambda, but I messed up the syntax for anonymous method and we were too pressed for time to google it.

Problem 2: "As simply as possible".
However simple as it may sound, this rule is all but simple if you start going down the road creating the wrong kind of simplicity. The TDD dogmatic anecdote is that you shall make a test pass in a manner that is as simple as possible.
This is to make sure that the unit tests enforce the functionality of the production code and that the tests truly drive the development process.

Just a week ago, I was attending TDD Masters class by Roy Osherove. I think that I grocked the concept of simplicity on the second day.

In my dependency use case, the first MMF is to make the controller start the FAN if the THERMOMETER shows a higher temperature than the desired temperature which you have set against the API of the HVAC controller instance.
Now, what exactly does simplicity mean? What should be the first test?

Does it mean to not interact with the mocked FAN?
Does it mean to not interact with the mocked THERMOMETER?
Why would it have dependencies at all? Can't you just put a temperature in and the ask it if wants to start the fan?

This started to take the form of a dug-in debate in the class, which I had to cut off.

I think that there is an important distinction to be made here. You do not program unit tests that you do not intend to make obsolete at a later time. Although the unit tests only run a concrete scenario testing a narrow case, they should still execute and pass against the final product three months from now. You still need to express the complete scenario against what you, at the time of writing, expect to become the final library API for that use case.
If you program against getters and setters and the getters only return true, you are not producing value as you plan to both throw away both the unit test and the production code that made it pass. You are also bestowing a legacy API on a brand new component.

I solve the problem as follows in my very first unit test: I create a mock fan and a mock thermometer (with a temperature), pass them both to the controller as constructor arguments and expect the fan to have been attempted to turned on.
This may seem like an advanced use case, but I think it is as simple as it can be.

People in the TDD domain also argue that you should start with constructor checks. At this point, I am not actually sure if it makes sense to be able to new up an controller without arguments, so I am actually forgoing that for the moment. If a later use case starts by newing up an empty controller, I will create a specific "IsNotNull" test.

Now, here is the correct meaning of "as simple as possible": When I confirm that the test fails, and I go into production code, I totally disregard the thermometer and I merely switch the fan on. I do not even store the thermometer reference which was passed in the constructor. This leaves people speechless. The thermometer is available, why not check it? You know that you will have to check the thermometer before starting the fan in the final product.....?
The key here is to recognize the fact that I would then be able to remove the check without breaking any tests. I would be able to introduce a bug in my code without causing any tests to fail. And if that happens, what good are they? How can I safely refactor the code if the green light does not mean anything?
Everybody agrees that I need at least two test cases to "nail" the functionality in place. And I need the if. And when people realize that, they also realize that I am not doing any extra work, I am just doing it in a different order. And the order has additional benefits because I do not have the overhead of writing unit tests after having the complete functionality in the production code.

If you write unit tests after the fact, you need to change the production code in order to see them fail. It is important that you know that the unit tests you write actually can and will fail.

To sum it up: "As simply as possible" does not mean "as stupidly as possible". You still play for keeps. The production code is "work in progress", but the unit test is supposed to be done, you don't plan on revisiting it. After all, it would be tedious process with a growing amount of unit tests if you planned on rewriting the API they all consume so that they no longer describe a valid scenario.

No comments:

Post a Comment