We've all heard the buzz acronym TDD. But has anyone actually put these things to use in a practical way? I know of a couple of guys who have, but for the most part, a majority of developers tend to shy away from this new fangled gadgetry. The main reason, I believe, is that it can be difficult to justify a learning curve when:
- A solution in a current ver. of code exists
- Management is on your ass to just "Get it done"
- Open source projects are a natural contraceptive
Now that I've stated the obvious, lets get on with it.
Requirements, everyone gets them, no one knows how to write them and they are usually so vague that you can tell someone has a huge ass their trying to cover. The biggest pains when it comes to requirements are, the acceptance of a "Moving Target" as part of business, and flaky UI designs in the document. Honestly, does Barbara, the manager of the mail room, really know what the hell a number spinner is or is she just a control freak who needs a coke and a bottle of horse ex-lax to work that stick out?
To put it bluntly, Requirements define the business logic that needs to go into the app. They are not justification for the app, that should have been done long ago. Neither are they "Screens" or "Mockups". Those get done by someone who actually (supposedly) knows what their doing. That may be you, it may not, depends on your shop. Either way, they don't belong in the document.
So, what do these obvious statements have to do with TDD?
Lets start at the top. Typically, in non-TDD terms, when you get the requirements, you start coding. Sure, you look at whats needed, pick out the sticky algorithms and look for logical objects, but ultimately, the first thing you write is:
Namespace Giveme
{
public class Beer
{
public int AndWomen()
{
Your mileage may vary, but typically you start writing code intended for production right away. And when the boss man speaks, you refactor refactor refactor. What happens after several refactors? Well, for one you've lost an easy way to CYA because none of your code matches the original requirements, it all matches what Boss man said. So now your left, holding the bag while Barbara and the Boss man say "We did our part, the developer is taking to long". At this point you get the "Talk" from someone who has no clue what it is you do, but by god, they know how to manage!
Enter TDD. It may not cover your ass completely, but it
may slightly, depends on how big an ass you are. Plus, you'll get your work done a whole lot faster. I know what your thinking...that writing unit tests is as painful as sitting the wrong way on a pineapple (is there a right way?), and it can be. Typically we've been introduced to unit tests by means of testing after development, which goes something like this
Manager: TDD is shiny. Bob, go write unit tests for all our legacy code
Bob: OK, but that will take me several months to complete
Manager: Nonsense, your just making sure working code works, Now, Kiss the ring and get me a pineapple. Then I expect 150% code coverage by tonight!
So Bob goes off and attempts to generate unit tests around the existing code. The existing code has some pretty complex methods which must be refactored out in order to test, so more tests are written. Come to find out the refactored methods need to be refactored again, and again and again....and new objects must be created. This cycle can (and in most cases does) create unstable code. This is what makes developers climb the bell tower, or have the sudden urge to become postmen (if you didn't get that last joke, just move along).
OK, so say your starting a new project. You've learned that unit testing after the fact leads to A LOT of re-factoring, and is a code destroying monster. So you want to avoid that at all costs. The solution? Write your tests first, get them out of the way. The simplest form of TDD is:
- Write Test
- Fail Test
- Write Code to pass test
- Pass test
- Refactor to nice code
- Go To : 1
Otherwise known as
1.) Red
2.) Green
3.) Refactor
So here is how it helps you. (I'm diggin' the numbered lists today)
1.) Unit Tests are tied to the requirements
2.) Writing tests first ensure you write ONLY the functionality you need
3.) 1 + 2 = quickly written solid code .
Here's how it works. First off, before writing any code for production at all, you break down your requirements into subsections, and then break those down. so on and so forth. Once your at the smallest bit of requirement (determining how much beer it would take for individual A to take individual B home in this instance), then you write a unit test that specifies how your method will act and look from the outside:
public void BeerGogglesTest()
{
//We really should atleast have a container class
BarFlys bf = new BarFlys();
//What we expect the method to return given the inputs
double expectedValue = 20;
//Test constants
//We'll talk about the whole "Mock" thing in a second.
int Dave= new MockPersonDave();
int DavesWife=new MockPersonDavesWife();
double BeersAlacContent = 7.5;
//Here we check
double HowManyBeers = bf.NumberOfBeers(BeersAlacContent, DavesWifes,Dave)
//Here we assert the value we received was the value we expected
Assert.IsEqual(expectedValue,HowManyBeers);
}
So...now you compile that. Yes, before writing any more code. Of course the test fails with a few exceptions. Your first task, get rid of those exceptions. With TDD, your essentially coding to your implementation, or API. Similar to coding to an interface. So every bit of your application comes down to ,"Write enough code, just barley enough, to legitimately pass the test"(notice i said Legitimately, so all you smart asses who would return 20 every time need a lesson pineapple-fu). In our scenario above, we would need to implement a public BarFlys class with one and only one method. That method would only take the 3 parameters listed. Don't worry about extensibility or any fancy OOP crap yet. That will come in time....maybe.
public class BarFlys
{
public double NumberOfBeers(double AlacConsistancy, IPerson p1, IPerson p2)
{
double p1DesireToLeave = CalculateDesire(p1.InitialInhibition,p1.alacholConsumed,p1.tolorance);
double p2DesireToLeave = CalculateDesire(p2.InitialInhibition,p2.alacholConsumed,p2.tolorance);
return Math.Sqrt((p2.CalculateDesireAbility(p1) + p2.DesireToLeave)/p2.tolorance)*p2.CommonSense
}
}
As an important aside, we must have already written tests for CalculateDesire in order for this to be a valid test run. We'd want to test it in the same manner we are testing "NumberOfBeers." If we didn't test those method first, then we may end up skewing our test results if there is a failure in one or both of them. Meaning NumberOfBeers would fail, but only because a Dependant method failed.
Once you pass that test, you move onto the next test. And continue on until you have a nice little set of tight, solid code. After wards, you begin to refactor. But, at this point, refactoring isn't about design changes. Your design has already been defined by your tests. Instead, you can focus on the real things, like optimizations and readability.
So, what are these "Mock Objects" I was going on about earlier? Well, Mocks can warrant their own blog post
in- and-of themselves. But here are the essentials. A mock object works just like a bottle of O'Dules or Coors Light, it's a fake implementation of a real solid object. Take this for example: You need to develop the front end to a website that runs exclusively off of web services. However, like most developers, the guy working on the services was hit by a bus or married a super model (your choice, i for one vote bus, that still leaves me the opportunity to meet this super model turned on by developers).
Management has threatened to stop paying you if you don't get off your ass and start writing that front end. The Solution? "Mock" the web services. Essentially, your going to write an object that mimics the behavior and implements the interface of what the web services are supposed to look like. Your also going to have those mock objects return data in the same format that the web services would.
Now, granted, it didn't take that many beers when I first met my wife. However, I think her common sense had some environmental issues to contend with, like she just saw dirty dancing or some other chick flick. Either way, I beat the odds :)