Wednesday, October 5, 2011

Unit Testing - A Pragmatic Approach

There's a lot of "should dos" out there regarding unit tests- Test Driven Development (TDD) is a concept a lot of professional developers get behind with an almost religious fervor, pointing to simple examples of how to implement a test-driven method which usually involves:
  • Write a test
  • Make the test fail
  • Write the code
  • Make the test pass
  • Profit.
...but really, this is completely impractical for a number of reasons:

1) Most compiled languages will require you to write the code first, otherwise the test will have nothing to actually test.  Writing a test for Foo() is great, except if Foo() doesn't exist, this will fail for the wrong reasons.  So building the test first doesn't always make a lot of sense.

2) Even simple methods have several tests that are applicable for each.  Consider the following code:

public bool Foo(string bar, int wee)
   {
   if (bar == "foo" && ((1 / wee) * 10) > 1))
      {
      return true;
      }
   else
      {
      return false;
      }
   }


How many conditions do we need to check for here, to see if the method:
  • Produces the proper output
  • Handles odd inputs gracefully
Let us count the ways:
  1. Test string bar for empty string condition
  2. Test string bar for null value condition
  3. Test string bar for "foo" condition
  4. Test string bar for "Foo" condition
  5. Test string bar for "Something Else" condition
  6. Test int wee for zero condition
  7. Test int wee for negative condition
  8. Test int wee for 5 condition
  9. Test int wee for "not 5" condition
Why so many tests?  For a nullable input, not testing a null value is asking for trouble.  Likewise, strings can easily be empty, and often when not expected.  These are the sorts of things that unit tests are meant to help with: testing for possible but unexpected data.

For integers, testing for zero is a given.  In the method above, a value of zero will throw a DivideByZero exception.  What if we were multiplying that number against a cost value, and someone inadvertently tossed an Int32.MaxValue as an argument?  Not testing for the default value of any variable is asking for trouble.  Likewise, negative numbers... maybe the code you are using this method with has a buggy math operation, if the application bombs in Foo() because it doesn't recognize the problems inherent in other code, you have failed at testing.  Edge cases are usually a good idea too. (max and min values)

I have 9 tests listed for method Foo.  And that's normal!  This is a fairly well-tested method.  You could take some shortcuts, but ultimately, you are looking at 3-4 tests per argument at a minimum, not to mention dealing with potential combinations that need to be handled gracefully.
    3) If you are in a technology-centric company, you likely have leadership that understands the importance of unit testing and quality control.  If, however, you are employed in the other 95% of businesses, your chances of having leadership that understands and appreciates this sort of effort dramatically decreases.  Technology people want accurate results; business people tend to want quick results, and the two rarely overlap.

    These are the reasons I have never been a big fan of TDD... but that doesn't mean I'm not a fan of unit testing.  Unit testing is an incredibly important part of any quality software production.  However, following doctrine for how unit testing should be done has resulted in slowdowns that bosses usually do not appreciate.

    Here are some ways I have made unit testing work for me and have given me a degree of confidence in the code I have produced:


    • Always test for default values of your argument types.  When coding, it's easy to declare/ instantiate an object, expect it to be properly populated, and then pass it into a method... only to find out that the object was not, in fact, properly populated.  Nullable objects should be checked for null values, integers should be checked for zero, etc.  Check for the values in your method, and throw the proper exception (ArgumentNullException or OutOfRangeException) when the data needs to actually be used.  Provide enough info to understand where the error occurred.



    • Keep your exceptions granular, so you can test for a specific condition, for example:


    bool Foo(string foo)
    {
       if (foo == null) throw new ArgumentNullException("foo");
       if (foo == string.Empty) throw new ArgumentException("parameter 'foo' cannot be empty")
    ...
    }


              In this example, we have two easily testable scenarios:

    [Test, ExpectedException(typeof(ArgumentException))]
    public void testFooEmptyStringArg()
    {
       var f = Foo(string.Empty);
    }

    [Test, ExpectedException(typeof(ArgumentNullException))]
    public void testFooEmptyStringArg()
    {
       var f = Foo(null);
    }



    • Test against interfaces as much as possible.  Internal workings of components should be tested, but the public usage of the classes and methods are the ones that will need the most scrutiny.
    • Manage your time.  Remember, in a perfect world, you'd have infinite time to create the perfect code and unit tests... but development is generally funded by business, and in business, time is money.  Start with the most obvious conditions to test, get a degree of confidence, and expect to come back to troubleshoot and fix bugs.  Whenever you fix a bug, add a unit test to make sure that bug never "reappears."
    • Always keep in mind the intent behind unit testing is not to create bug-free code, nor is it to provide a troubleshooting mechanism for bugs.  It is a validation tool to confirm that your code behaves the way you meant it to... and when you have to change your code base, well-written unit tests will show you what your latest changes have broken in other places, which will drastically reduce regression testing and user acceptance time.
    That's a basic overview.  Comments are welcome (I expect a few will be how wrong I am)...

    Tuesday, October 4, 2011

    Entity Framework - A Review in Practice

    I started playing with EF with .NET 4.0.  I came into it with a reserved attitude - my coworkers shoehorned it into a new project we were working on; I was more comfortable with ADO.NET, and was skeptical that it would make our lives easier as it was supposed to, but as a professional geek I have a responsibility to give new tech a fair shake.  Now I have used and am currently using EF for a number of projects that I am working on, and I feel comfortable giving my two cents about the product.

    Coming from a strong DB background, I prefer to model in the database environment.  Conveniently, the EF model generator supports this.  You build your schema, define your keys, create your relationships and stored procedures, and then, once your data model is complete, use EF to generate your object model for use in code.  Invoke a couple T4 templates, and you're off, cooking with gas.

    The support for this is pretty good as long as you are not doing anything esoteric in your data model; of course, in almost any business application, there is usually a few things that are... off.  Business object models rarely have the consistency that data object models do.  The instant you step off the beaten path with EF, you are in a world of headaches.  The one-offs are dangerous, because all that time you spend just trying to get your model to validate eats up what you would have saved if you had used a more primitive but flexible mechanism.

    Trying to get a model to validate with one small difference from what EF thinks you actually want has caused me to scream at my computer loud obscenities at all hours of the night.  Good thing I live alone.  The EF interpreter of the schema is sophisticated, but not flexible, and if you are fairly versatile in the way you build your schema, EF is likely to frustrate the hell out of you.

    All bitching aside, one of the good things about EF is if, as you go along, you realize you need to add a column to a table that is referenced in six other spots, you can, with very little difficulty.  This is a huge timesaver over editing a dozen stored procedures, and why I still use EF despite the effects it has on my blood pressure.

    Integrating your object model into a UnitOfWork pattern is fairly straightforward, although I have to admit the first few times we tried we way overthought it until we'd painted ourselves into a corner, and had to rip it all out and start over again; I think this had more to do with us not really understanding the UnitOfWork pattern, though, and less with EF.

    One problem I had with EF was the way it handles child objects in the graph.  The .Include() method adds the child objects to the target object from the repository with the relative ease you would expect from an ORM; God help you if you want to alter those child objects as they relate to the parent object, though.  It's possible to do, but completely counter-intuitive, and detracts from the benefit of the ORM model, which allows us to treat data as objects since we tend to think that sort of way.  With all of the automation involved in the underpinnings of the EF object repositories, you'd think they'd have a much more elegant way of handling this... but they don't.

    Another issue I had was with portability.  There were times I wanted to take a full object graph and transport it to another system.  The actual data was serializable; the underpinnings of the "glue" that held the object properties together (navigation properties) were not, so much.  To me, it would make perfect sense to allow the developer to define a key translation behavior (do I keep this identity-based key, or make a new record?) and insert/update an entire graph from another system.  Portability and generics are not well supported in EF.

    The biggest thing I don't like about EF is there is no concept of a "JDI button"... where JDI stands for "Just Do It".  EF will try to validate the model against the database, and against its own internal configuration, but it's very sensitive, and if it encounters a case where it imagines something could go wrong, it will not allow you to proceed, even if you know the condition it is worrying about will never occur, based on business logic or the data being transformed before it got to that point.  Everything is an error, and the errors are cryptic and vague... and not well documented.  Just make it a warning, let me deal with the problem when (if) it actually is a problem.  Please!

    An interesting "bug" I've seen with EF is the way it sometimes handles relationships... there are times when several foreign key relationships are designed in exactly the same manner, but are interpreted differently in EF.  A manual tweak will make the problem go away, but it's bizarre to see it in the first place.

    The way the EF lays out the object model is nothing short of bizarre.  Luckily, you can reorganize the model, and it will remember your change.

    All in all, I am not happy with EF in its current incarnation, but I still use it because for the most part it is better than the alternative.  Still, by version 4 I'd expect a far more mature product.