One Small Step Toward TDD, One Small Leap for SEP

April 3, 2012

One small Step Toward TDD

I have been reading about TDD and unit testing for about 7 years now and have not fully adopted TDD. It has a ton of time tested benefits including writing less production code, writing simpler code, making your code easier to change, making your code easier to test and the list goes on. I often ask myself, why? Is it because the guy next to me does not do it? Is it because I am lazy? Is it because it is hard to use in real life? I already write unit tests after I write my code and have seen the benefits of this when changing the code. So why is it so hard to write the tests first?

I started to get some answers to this question from a post I recently read https://davesquared.net/2011/03/why-learning-tdd-is-hard-and-what-to-do.html. I think my problem is that when I think of TDD I always assume that I need to write tests before writing the code. That is not always the case. If I am adding to a preexisting design, for example, adding a search page to an existing website will most likely result in following an existing architecture to fill in the missing pieces. In this case writing tests before writing the code is not TDD, it is Test-First development, although Test-First development has its benefits too.

TDD is used to get feedback on the design of your code. Recently I wanted to use a third party tool on my project but I did not know how to use the tool. The tool is called “The Rules Engine” that was developed at SEP. The basic idea of the Rules Engine is that it takes a DataTable, performs a set of rules on each row, and stores the result in a column that was defined by the “Rules”.

First I knew I wanted to use the Façade pattern to abstract the details of interfacing with the rules engine from the rest of my code. Second I had to ask myself some questions like, what is the responsibility of the wrapper? How do I want to call the wrapper? What are the outputs of the wrapper? How can I tell the wrapper is working correctly? What happens if the format of my rules are malformed?

So to answer my questions I started to write tests like the one below. I wanted to give the wrapper my rules definition, which is to subtract three columns called “Test”, “Test1” and “Test2” and put the result in a column called “calculated-column”.

1. [TestMethod]2. publicvoid ShouldReturnTwoWhenSubtractingThreeNumbers()3. {4.     var wrapper = newRulesEngineWrapper();5.     conststring ruleDefinition =6.         @”7.                 name=””calculated-stuff””>8.             9.                 10.                     11.                         12.                         13.                         14.                     15.                 16.             17.         18.                      19.     “;20.     var dt = newDataTable();21.     dt.Columns.Add(“Test”, typeof (double));22.     dt.Columns.Add(“Test1”, typeof (double));23.     dt.Columns.Add(“Test2”, typeof (double));24.     dt.Rows.Add(7, 4, 1);25.     dt.Rows.Add(7, 3, 1);26.     var resultsSet = wrapper.ExecuteRuleOnDataTable(27.         ruleDefinition, dt);28.  29.     Assert.AreEqual(2.0, resultsSet30.         .Rows[0][“calculated-column”]);31. }

Then I wrote the simplest code I could think of to make the test pass.

1. publicDataTable ExecuteRuleOnDataTable(string rules, DataTable data)2. {3.     var table = newDataTable();4.     table.Columns.Add(“calculated-column”);5.     table.Rows.Add(2.0);6.                7.     return table;8. }

Since this is not very useful for other uses outside of the first test I wrote another test that adds two columns and puts the result in “calculated-column”.

1. [TestMethod]2. publicvoid ShouldReturnThreeWhenAddingTwoNumbers()3. {4.     var wrapper = newRulesEngineWrapper();5.     conststring ruleDefinition =6.         @”7.                 name=””calculated-stuff””>8.             9.             10.                 11.                     12.                     13.                 14.             15.             16.             17.                            18.     “;19.     var dt = newDataTable();20.     dt.Columns.Add(“Test”);21.     dt.Columns.Add(“Test1”);22.     dt.Columns.Add(“Test2”);23.     dt.Rows.Add(1, 2, 1);24.     var resultsSet = wrapper.ExecuteRuleOnDataTable(25.         ruleDefinition, dt);26.  27.     Assert.AreEqual(3.0, resultsSet28.         .Rows[0][“calculated-column”]);29. }

Since I am expecting 3.0 and not 2.0 I need to call the rules engine and try to use the least amount of code to get the job done.

1. publicDataTable ExecuteRuleOnDataTable(string rules, DataTable data)2. {3.     _data = data;4.     rules = string.Format(rules, DataSourceId, OutputHandlerId);5.     var rulesBag = string.Format(RulesTemplate,6.         RuleSetName, DataSourceId, OutputHandlerId, rules);7.  8.     using (Stream ruleDefinitions =9.         newMemoryStream(rulesBag.ToByteArray()))10.     {11.         var repository = RulesEngineFactory.Create(ruleDefinitions);12.         var ruleSet = repository.GetRuleSet(RuleSetName);13.         ruleSet.Context.DataSources[DataSourceId] = this;14.         ruleSet.Context.OutputHandlers[OutputHandlerId] = this;15.         ruleSet.Run();16.     }17.     //waiting for rules engine to Notify us that it is done, it will call NotifyResults18.     var r = _results;19.     while (r == null)20.     {21.         r = _results;22.         Thread.CurrentThread.Join(1000);23.     }24.     return _results;25. }26.  27. publicvoid NotifyResults(DataRow[] resultSet, IRuleContext context)28. {29.     _results = resultSet[0].Table;30. }

After I was confident that my “happy path” worked correctly I stared asking myself questions like:

  • What happens if the rules parameter is in a bad format?
  • What happens when the rules are an empty string?
  • What happens if the data parameter is an empty DataTable?
  • What happens if the rule has a No-Op rule?
  • What happens if the rules are null?

I wrote tests that addressed each of these questions. After writing each test I ran it to make sure it failed correctly, and then wrote the code in my ExecuteRuleOnDataTable method to make the test pass.

TDD helped me:

  • Take a problem I had and isolate it.
  • Forced me to make some design decisions before writing any code.
  • Helped me understand a fairly complicated tool in a short amount of time.
  • Forced me to think about edge cases of calling the rules engine before I integrated it with the rest of my code.
  • Helped my create a class that was simple to use for a complex tool.

If I did not use TDD for this problem it would have taken me a lot longer to implement. I would have had to integrate a tool that had no documentation and a couple of examples on how to use it in the web page I was implementing. Chances are I wouldn’t have gotten the rules quite right so I would have had to go through a couple of iterations of changing the rules (that were stored in a database) loading the webpage, etc, and it would take even longer if I missed an edge case somewhere, well you get the point.

One Small Leap for SEP:

What if everyone on your team approached each software problem this way? I think you would find that you would get simpler code that was easier to change.

So why would I want to make my code easy to change? Well if you have been in the software industry for more than 30 seconds you know software requirements change all the time. Sometimes a client won’t know something is right until they see it, which is OK, but wouldn’t it be great if your code was already written to support change and you could make the change without much effort?

The project that I am on was originally written in Perl more than 10 years ago. New pages added to the website are written in .Net. Although my team does a pretty good job surrounding the code with unit tests, when we have to change some of the Perl code we have to do it without automated unit tests that would make sure our change does not break something else. Yikes! Sometimes we need to change code that no one on the team even knows what the original intent was, however, if there had been a test; we would know what the behavior was supposed to be. Changes to this code generally take longer because we have to find where in the code we need to change, then we need to determine if our change is going to affect other pages, then we need to actually make the change, come up with some test cases to manually walk through, and make sure there is a good data set in the database.

Please write your code so it can be changed easily. If you are the one changing it years after you wrote it you will thank yourself for documenting what the code does with a unit test (I do this all the time, unless it is Perl code I wrote then I ask myself why didn’t I write a TEST!). If it is someone else that needs to change it they will thank you. Also, your clients will appreciate that a change can be done so quickly.