Hello there! It looks like this might be your first time to my website. You should...
Subscribe to my
RSS feed
Follow me
on Twitter.
Check out
.NET Dev Buzz

TDD Space Invaders Video and Downloads

Wednesday, November 04, 2009 11:17:00 PM (Pacific Standard Time, UTC-08:00)

Recently Llewellyn Falco and I did a webcast for DevelopMentor where we demonstrated some TDD techniques and introduced Approval Tests. We let the audience choose our project and they chose Space Invaders. It was all great fun. Now the videos and MP3 streams are online and available for download.

Watch streaming video (WMV HQ)
Watch streaming video (WMV HQ)   Download WMV Video Listen to MP3 Streaming Download MP3

Be sure to check out the write-up we did afterward where we talked about the tools and gave you a chance to try it for yourself:

   TDD Space Invaders Write-up

You can also watch two other, higher level agile webcasts by Bill Nazzaro here:

   Agile Webcasts at DevelopMentor

Cheers!
Michael

Tweet this Follow me on Twitter Post this to dotnetshoutout.com Digg this Submit this to Stumbleupon email this post

TDD Invades Space Invaders

Wednesday, October 28, 2009 1:13:21 PM (Pacific Standard Time, UTC-08:00)
A joint post by Llewellyn Falco and Michael Kennedy

[Update: Get the videos and additional downloads for this webcast.]

As a follow-up to our "Avoiding 5 Common Pitfalls in Unit Testing" article we did a webcast where we took a problem from the audience and solved it live and unrehearsed on stage. These kinds of performances are always a risk but that's part of what makes them fun.

Of course, the question is could we have done it better? Here's your chance to try it for yourself (details below).

The Problem:

Our viewers chose to have us build the game Space Invaders. The first thing we had to do to sketch out a basic scenario we could implement. We started with a picture to remind what Space Invaders even was:



This was too big of a scenario for us to tackle in the allotted 40 minutes for programming. So then we started by creating a simpler scenario which we sketched out on the "whiteboard":


               Click for full size image.


Flushing Out the Scenario
:
In doing this, a couple of things were revealed about the game.

First, we wanted to make the tank and aliens all be the same size so we could put them on a grid. But then we saw that our bullet wouldn't fit that story, so we introduced the idea of relative sizes. We also realized that even though we drew the block, it was too complex for the first scenario and it would have to wait.

Notice that as we started writing the scenario in English, there are mistakes, irrelevancies, and problems with the order. This is OK. The thing to remember is that all of this was done for the sole purpose of creating a recipe for a scenario we could test. That scenario is the following:
[TestMethod]
public void TestSimpleKill()
{
   // 1. Create a 15x10 board.
   // 2. Place a 3x2 tank at 1x8.
   // 3. Place a 2x2 alien at 7x3 heading west.
   // 4. tank shoots
   // 5. advance 4 turns
   // 6. not won
   // 7. advance
   // 8. win
}

Now that we had the recipe, we could go about writing the code.

Here's your chance to play at home!

  1. Set your timer to 40 minutes.
  2. Create a new test project.
  3. Paste that method above.
  4. Translate the comments into code.
If you believe there's a better process, we invite you to try that as well.

We made it to step 4 during our presentation (download code below) and estimate another 15 minutes would have had the whole scenario done, tested, and well-factored.

Stories vs. Requirements (stories win):

We'd like to point out a couple of things about the story. First, it was quick to write the story. We did it in 5 minutes. Second, it translates well to code because it has behavior and objects working together. Let's compare that to the requirements that this story flushed-out.
  • Need a board
  • Boards should have width & height
  • Boards contain game objects
  • Game objects have a witdth, height
  • Game objects have the ability to move each turn
  • Aliens move either left or right each turn
  • Bullets move either up or down each turn
  • Bullets are 1x1
  • Tanks are 3x2
  • Aliens are 2x2
  • The game is not won until all the aliens are killed
  • The game is won if alll the aliens are killed
  • An Alien is killed if it is hit by a bullet
  • Tanks can fire
  • Firing with a tank creates a bullet going up from the space directly above the tank

Now we want to point out that this requirements doc is much hard to understand than our story. For example, if you were to add more requirements (e.g. an alien also shoots) is that easy to determine whether we have complete requirements? It also takes much more effort to create and especially to tell if it is complete. People aren't made to handle requirement documents well but we are story-telling machines. We embrace this in our coding techniques.

We'd also like to mention some of the tools discussed at the end.

For remote collaboration we use:

Skype (audio / video)
VNC for screen keyboard sharing
RDP (windows remote desktop) -- requires Windows 2003/2008 server for pairing.

Source Control:
TortoiseSVN
TortoiseGit

Developer Tools:
Resharper
CodeRush

Testing Tools:
MsTest (in Visual Studio Professional and up)
NUnit
NCover
TortioseDiff
Approvals Tests
Approvals Tests CodeRush add-in
Rhino Mock
TypeMock

If you try this scenario yourself, please leave a comment about your experience.

Download the code and slides from the webcast here:

   Code: TddWithLlewellynAndMichael.zip
   Slides dmtdd.pdf

Cheers -- Michael and Llewellyn

Tweet this Follow me on Twitter Post this to dotnetshoutout.com Digg this Submit this to Stumbleupon email this post

Attend My Live, Free TDD Webinar with Llewellyn Falco and Myself at 10am Tomorrow!

Monday, October 26, 2009 3:29:46 PM (Pacific Standard Time, UTC-08:00)

[Update: See the follow-up post here: TDD Invades Space Invaders]

Tuesday, October 27, 2009 at 10am Pacific time Llewellyn Falco and I will be giving a live, unscripted, and no safety-net demonstration of Test Driven Development (TDD) as part of the DevelopMentor webinar series (this particular series is a 3-part series on Agile development).

We already have a bunch of attendees registered. But we have room for as many of you who are interested in agile and TDD. Sign up here:

   http://bit.ly/dm-tdd-m-and-l

In addition to core TDD techniques, you will see how an amazing technique and set of tools designed by Llewellyn called Approval Tests makes writing tests as simple as verifying an image or text file. Tired of writing 50 lines of test code for every 50 lines of production code but you still want the power of TDD? You need to learn more about Approvals and we'll demo that live tomorrow!

I hope to see you all online. Feel free to help me get the word out by tweeting this or shouting it (see icons below).

Cheers, Michael.

Tweet this Follow me on Twitter Post this to dotnetshoutout.com Digg this Submit this to Stumbleupon email this post

Article: Avoiding 5 Common Pitfalls in Unit Testing

Friday, August 07, 2009 4:40:13 PM (Pacific Standard Time, UTC-08:00)
Llewellyn Falco and I recently wrote an article for DevelopMentor's Developments newsletter entitled Avoiding 5 Common Pitfalls in Unit Testing.

You can read it at the DevelopMentor website:

   http://www.develop.com/testingpitfalls

I've republished here for my readers. Enjoy!

[Update: We have also done a webcast demonstrating some of these ideas, which we wrote up here:
http://www.michaelckennedy.net/blog/2009/10/28/TDDInvadesSpaceInvaders.aspx]

Avoiding 5 Common Pitfalls in Unit Testing

by Llewellyn Falco and Michael Kennedy

When I started out with unit tests, I was enthralled with the promise of ease and security that they would bring to my projects. In practice, however, the theory of sustainable software through unit tests started to break down. This difficulty continued to build up, until I finally threw my head back in anger and declared that

"Unit Tests have become more trouble than they are worth."

So we stopped. Not all once, but over the months our unit tests died a quiet death. When tests would stop working, we just ignored them. When new features were reported, they were developed without unit testing. At first, it seemed great. We were able to move without the baggage of maintaining the old tests! But soon all the original problems of having a system without tests came back to us. Things keep breaking, deadlines were increasingly pushed back. Releases came with an extraordinary amount of stress, late nights & weekends. The final straw came when we were forced to rush out an immediate update, and ended up taking down the company for 2 days straight. Our new motto became:

"Unit Testing: you're damned if you do, you're damned if you don't."

In the end, we decided that despite the hardship caused by maintaining unit tests, it just wasn't feasible to operate without them. So we started down the road to re-incorporate testing into our software development process. As the months went by, however, we discovered that the hardships we remembered had not returned. Looking back, we realized that we had made many mistakes the first time around. The second time around we were smarter. So you, too, can enjoy the benefits of unit tests here are the 5 major pitfalls we encountered the first time around, and how you can avoid them.

Pitfall #1: Tests are hard to maintain.

Because tests were only there to service and support the production code, they became second class citizens. We would spend time carefully choosing method names, refactoring our code to keep our classes and methods small, and so on. But we never applied these same principles to our test code.

As a side-effect of adding back the old tests, we reviewed and cleaned them up with the same level of scrutiny we gave to our "real" code. Suddenly the tests were easier to maintain. While this should not be a surprise to anyone, it wasn't util this moment that we realized why they had been so hard to maintain in the first place:

Our tests were hard to maintain because we weren't maintaining them.

Solution: Going forward, we expect the same quality of code (or higher) in the unit tests as we do for our production code. That means
  • We remove duplication
  • We carefully consider method names
  • We create convenience functions for testing features
  • We keep our methods short
  • We code-review our unit tests

Pitfall #2: Tests are lot of work to write.

We found that in order to test even simple things we would have to write lots of code to setup and execute the scenario. Even something simple like "create a new user, and receive welcome email" would turn into 40-50 lines of step by step instructions.

Not only was this a pain to write the 1st time, it became a nightmare to maintain. Little changes would mean re-reading those functions to detect if the test was failing because we broke something, or simply because we had changed something. Once that was discovered, we would then have to update the now out of sync test code.

The solution we actually found may surprise you. We found that writing out our tests in English and then translating each line into 1 line of code naturally created the appropriate levels of abstraction and readability.

For example, let's consider testing the following scenario:

Who are you receiving the most email from?
  1. Create you - the user
  2. Mike sends you 3 emails
  3. Mary sends you 4 emails
  4. Joan sends you 2 emails
  5. Verify your greatest "emailer" is Mary
This will naturally lead us to write the following test method and helper method:

[TestFixture]
public class AccountTests
{
    private MockMailServer mockMailServer = new MockMailServer();

    [Test]
    public void WhoAreYouReceivingTheMostEmailFrom()
    {
        User you = User.CreateNew( "you" );
        User mike = SendEmailHelper( CreateUser( "mike" ), you, 3 );
        User mary = SendEmailHelper( CreateUser( "mary" ), you, 4 );
        User joan = SendEmailHelper( CreateUser( "joan" ), you, 2 );

        Assert.AreEqual( mary, you.GetGreatestEmailer() );
    }

    private User SendEmailHelper(User from, User to, int quantity)
    {
        for ( int i = 0; i < quantity; i++ )
        {
            EMail mail = new EMail()
            {
                To = to, 
                From = from, 
                Body = "Sample",
                Subject = "test"
            };

            mail.SetFormat( Formats.Html );
            mockMailServer.Send( mail );
        }
        return from;
     }

Notice that to a programmer, the lines of code in the WhoAreYouReceivingTheMostEmailFrom test are as easy to read as the lines of English were. We were naturally motivated to create the "SendEmailHelper" function because that was required by one-to-one correlation between the lines of English and the lines of test code. However, without that helper, our test would have become an unreadable rat's nest. This also naturally removes some duplication, increases maintainability, and allows for some reuse of the test convenience functions. This won't be the only test that requires us to send email; for example, we may want to test that we can find out to whom you sent the most email.

Let's compare this to how our tests would look if we had just hacked out the scenario:

[Test]
  public void WhoAreYouReceivingTheMostEmailFrom()
  {
      User you = User.CreateNew("you");
      User mike = CreateUser("mike");
      for (int i = 0; i < 3; i++)
      {
          EMail mail = new EMail{To = you,From = mike, Body = "Sample", Subject = "test"};
          mail.SetFormat(Formats.Html);
          mockMailServer.Send(mail);
      }
      
      User mary = CreateUser("mary");
      for (int i = 0; i < 4; i++)
      {
          EMail mail1 = new EMail{To = you,From = mary,Body = "Sample",Subject = "test"};
          mail1.SetFormat(Formats.Html);
          mockMailServer.Send(mail1);
      }
          
      User joan = CreateUser("joan");
      for (int i = 0; i < 6; i++)
      {
          EMail mail2 = new EMail {To = you, From = joan,Body = "Sample",Subject = "test"};
          mail2.SetFormat(Formats.Html);
          mockMailServer.Send(mail2);
      }
      
      Assert.AreEqual(mary, you.GetGreatestEmailer());
      }

Because we wrote the first version in English it's also easier to detect a mistake. You may have noticed that the second example had a typo at line 31, making Joan the biggest emailer. In general long methods have the disadvantage of obscuring intent. Unfortunately the 'follow a script' aspect of testing lends itself to writing long methods. By writing the tests in English and then doing a 1-to-1 conversion to code we can counter this vulnerability.

Write the tests in English before you code them.

Pitfall #3: Adding a new feature breaks a lot of tests that I then need to adjust.

I always dreaded adding new features because I knew it meant the existing tests were going to complain about the changes. It seemed like the tests themselves were resisting change to my system, rather than supporting it. As I made changes and the tests broke, I was always trying to figure out if those changes were "expected" because of the new feature, or unintended bugs I had introduced into my software.

Nowadays, we always prep the system for the new feature. This allows us to isolate 'expected' changes from unintended bugs. Furthermore, once we have finished prepping for the new feature, we find it extremely easy to actually add that new feature. Best of all, if the unit test breaks now, we know it's because of unexpected side effects of our changes.

This 'prepping' period falls under the title of 'refactoring' and requires the simple rule that during this stage you do not change the behavior, only the implementation. This sounds straight forward and simple, but in practice it requires a great deal of discipline.

Personally, I still find it a challenge to NOT fix a bug discovered during refactoring. I have to force myself to leave it alone and wait until I have finished refactoring before changing (Fixing) this behavior, but this discpline has paid off many, many times.

During this period, the support provided by your unit test suite really shines. Those tests allow me to rework my code with confidence. Afterwards the architecture in place has been custom designed to support the addition of this particular new feature, thus making its implementation quite straightforward. In our experience the 'prepping' work tends to actually take more time than we spend adding the actual feature itself, but the total time to implement is much less.

By spliting the work into two phases, we can emphasize the fact that the unit tests are supporting the existing system allowing its architecture to evolve so that extending it does not become increasingly difficult.

Before I would ask myself "How can I add this new feature?" Now I ask "How can I make it so this new feature will be easy to add?"

Prep the system for the new feature first.

Pitfall #4: When I change something a whole bunch of tests break even though I haven't broken the system.

There are many ways to solve the same problem. In the past, we tended to test a specific implementation of a solution instead of testing that we had a solution. Because we were focused on the specifics of implementation, changes to our solutions kept breaking our tests, even though we still had a valid solution. Moreover, because the tests were closely tied to implementation, rediscovering the intent and separating it in the tests became cumbersome. As we became more proficient at writing tests in English, we were able to create unit tests that described the expected behavior. This conveys a higher level of intent, and made the tests much less brittle.

Let's look at an example:
[Test]
  public void TestGatewayCallSuccessful()
  {
      var gateway = new Gateway {Mask = "ExampleCode.*"};
      var enviroment = new Dictionary();
      enviroment.Add("path", "ExampleCode.HelloWorld");
      string result = gateway.ExecuteRequest(enviroment);
      Assert.IsTrue(result.Contains("Hello World"));
  }
  
  [Test]
  public void TestGatewayBlocksInvalidMasks()
  {
      Assert.IsFalse(Gateway.IsValidForMask(
          "Example.*", "ExampleCode.HelloWorld"));
          
      Assert.IsFalse(Gateway.IsValidForMask(
              "ExampleCode.*.Extras.*", "ExampleCode.HelloWorld"));
              
      Assert.IsTrue(Gateway.IsValidForMask(
              "ExampleCode.*", "ExampleCode.HelloWorld"));
  }


In this example, we wrote our second test TestGatewayBlocksInvalidMasks so we could easily test a few examples to make sure our implementation was correct. In doing so we exposed a method IsValidForMask, which was an implementation detail and was only made public in order to make testing easy and intentional. We did this because actually executing something to get the failure was much more involved as evidenced by the first test (TestGatewayCallSuccessful).

Let's take a look at the specific solution we've come up with:

public class Gateway : IRunner
  {
      public string Mask { get; set; }
      
      public String ExecuteRequest(Dictionary environment)
      {
          string path = environment["path"];
          AssertValidClass(path);
          IRunner instance = 
                (IRunner)Activator.CreateInstance(Type.GetType(path));
          
          return instance.ExecuteRequest(environment);
       }
       
       private void AssertValidClass(string path)
       {
          if (!IsValidForMask(Mask, path))
          {
              throw new Exception(String.Format(
                    "Invalid Path '{0}' for Mask '{1}'", path, Mask));
          }
       }
       
       internal static bool IsValidForMask(String mask, String path)
       {
           mask = mask.Replace(".", "\\.").Replace("*", ".*");
           Regex regex = new Regex(mask);
           
           return regex.IsMatch(path);
       }
 }

As we can see, even though we are only creating this gateway once each time a call to ExecuteRequest is made we have to recreate the regex expression (line 31 & 32). It would be nice to do this just once. Let's take a look at a more efficient solution:

public class Gateway : IRunner
  {
      private string mask;
      private Regex regex;
      
      public string Mask
      {
         get { return mask; }
         set
         {
            mask = value;
            regex = new Regex( mask.Replace( ".", "\\." ).Replace( "*", ".*" ) );
         }
      }
      
      public String ExecuteRequest(Dictionary environment)
      {
           string path = environment["path"];
           AssertValidClass(path);
           IRunner instance = (IRunner) Activator.CreateInstance(Type.GetType(path));
           
           return instance.ExecuteRequest(environment);
       }
       
       private void AssertValidClass(string path)
       {
           if (!regex.IsMatch(path))
           {
               throw new Exception(String.Format("Invalid Path '{0}' for Mask '{1}'", path, Mask));
           }
       }
 }


Unfortunately this refactoring breaks the first set of tests. Notice that the function we are calling no longer even exists. However, let's look at what happens if we write our tests for the behavior rather than the implementation:

[TestFixture]
  public class GatewayBehaviorTests
  {
     [Test]
     private void TestGatewayCallSuccessful()
     {
         string result = Run("ExampleCode.*", "ExampleCode.HelloWorld");
         Assert.IsTrue(result.Contains("Hello World"));
      }
      
      [Test]
      public void TestGatewayBlocksInvalidMasks()
      {
          AssertValidForMask(false, "Example.*", "ExampleCode.HelloWorld");
          AssertValidForMask(false, "ExampleCode.*.Extras.*", "ExampleCode.HelloWorld");
          AssertValidForMask(true, "ExampleCode.*", "ExampleCode.HelloWorld");
      }
      
      private static String Run(string mask, string path)
      {
          var gateway = new OptimizedGateway {Mask = mask};
          var enviroment = new Dictionary();
          enviroment.Add("path", path);
          string result = gateway.ExecuteRequest(enviroment);
          return result;
      }
      
      private void AssertValidForMask(
      bool exceptionExpected, string mask, string path)
      {
          Exception found = null;
          try
          {
             Run(mask, path);
          }
          catch (Exception ex)
          {
             found = ex;
          }
          Assert.AreEqual(exceptionExpected, found == null);
       }
}

There are a few things to notice now:
  • This test not only works for the new code, but the old code as well. This is because the behavior has not changed, just the implementation.
  • We have not sacrificed readability or clarity of intent. In fact the first test TestGatewayCallSuccessful has actually gained readability.
  • There is the introduction of helper methods in the unit test. We find that this is a side effect of writing tests for readability and intention.
In the end, we realized there is a particular code smell for this problem.

If a different implementation of a solution requires different tests, you are testing to the wrong level.

Pitfall #5: There are just too many possibilities to test

When we first began unit testing, we felt that we had to test as many inputs as possible because we believed the purpose of the unit tests was to ensure complete quality of our code. What we have learned is that the world is not black and white, and neither is testing. It is not the case that we either have verified code or unverified code. There are levels of protection. In fact there is a level at which you get diminishing returns from new inputs and, surprisingly, that number is often very small.

For example: Imagine the following scenarios :

Scenario 1
You have a method
public int doSomething(int a, int b) {/*...*/})
Does this method work?
Will it blow up if I run it?
On a scale of 1-10 what is your confidence level?
Confidence Level 2: In our case, our confidence started out at 2. All we know is that it compiled. Any number of things could be wrong..
Scenario 2
Now, assume you have an invocation of the method
doSomething(2,3);
When you run this, it does not crash although you have no way to check its result.
What's your confidence level now?
Confidence Level 6:  As soon as it's been executed, our confidence jumps up to a 6. We know that most bugs come from incorrect wiring, or null pointers, and so on. Now we know it's not blowing up, but still don't really know that it's working
Scenario 3
Now imagine that you have a test
assertEquals(8, doSomething(2,3));
This test passes.
What's your confidence level now?
Confidence Level 8:  Just a single confirmation pulls us all the way up to a confidence level of 8. Notice that we are still just at 1 test case. A few more and we'll be in the 9's, but how many more cases would you need to say with absolute confidence that this works? (hint: 2^32 * 2^32).
     
Tests are like seatbelts: just because they won't guarantee your survival in all crashes, it doesn't mean you shouldn't wear them. Take the extreme case of a motorcycle helmet. You are only protecting a small part of your body, but you are significantly improving the odds of survival if something goes wrong.

A general rule of thumb for the number of cases to tests is "3 is a big number".
  1. Test the happy path
  2. Test an edge case
  3. Test an error case, if you have one
Start with the happy path. If you still are worried, try an edge case. Wait until a problem presents itself before you test further.

Spend your time where it counts.


Tweet this Follow me on Twitter Post this to dotnetshoutout.com Digg this Submit this to Stumbleupon email this post

Significant Advances in Unit Testing Windows Workflow

Sunday, January 18, 2009 11:57:03 AM (Pacific Standard Time, UTC-08:00)

This post describes a unit testing library for testing Windows Workflow Foundations. It is not a framework like HarnessIt, NUnit, or MsTest. Rather it's a library that can be used in conjunction with any of these testing frameworks.

Download the library with sample test project here: Kennedy.WorkflowTesting.zip (216 KB)

You can also just jump to the code.

First a Little History:

Last September I posted this teaser entitled Unit Testing Coming to a Workflow Near You. My intention was to post this article that you're reading now shortly thereafter when I got some free time to polish things up. In that previous post, I highlighted what I could determine to be the current state-of-the-art with regard to unit testing workflows, circa September 2008.

Then I heard through some inside sources that this MSDN Magazine article was about to come out:

     Foundations: Unit Testing Workflows and Activities by Matt Milner.

So I decided to see what Matt's article had to offer to the conversation. It's a good article to be sure. It covers a lot of the things I thought were undiscussed and yet important to unit testing WF (e.g. using WF services as points of dependency injection for mocks and stubs). Thanks Matt! I don't have to write about that now, but you'll see it used in the sample with my library.

What I was really waiting to see was would that article make this post redundant? After reading it, I can say that there's still a long way to go - and this library will get us most of the way there. Now let's get to the good stuff!

Significant Advances:

That's a pretty bold statement, significant advances: let's see if I can back it up. Here's what's missing in one way or another from all the previous work on unit testing WF. (Please note that this discussion is in no way intended to belittle the work of anyone quoted above, just to build on their work and advance testing for us all).

Problems with unit testing WF today that are solved by this library:

  1. Testing single activities: Testing single activities in isolation is hard.
  2. References to the activity: Direct access to the activity under test for asserting on its properties is nearly impossible.
  3. Waiting on workflows: Workflows run on background threads which means waiting for the outcome inside the test method is more cumbersome than necessary (ManualWorkflowScheduler is unnecessarily cumbersome as well).
  4. Untyped name/value collections as input: Using untyped name/value collections as input and return values is error prone (for testing and general use).
  5. Expected exceptions: Testing "failure as success" cases for error handling is essentially broken for WF: Either the exception type is lost, or the call stack is lost, or both.

The Sample:

Let me set the stage first before we see the test code. I have a somewhat realistic workflow which will exchange two stocks and either debit or credit your bank account with the difference. So you might want to sell 5 shares of Google and buy 10 shares of Microsoft and pocket the difference.

Here's the workflow which involves 4 different activities:

Testing On Single Activities:

The first thing to test is the individual activities (like BuyStock and DebitAccount). Here's the code to test selling a stock (some details omitted for simplicity, exact code follows later). This method uses my library class WfRunner for executing the test.

[TestClass]
public class WorkflowTests : IDisposable
{
    private WfRunner wfRunner = new WfRunner();

    [TestMethod]
    public void SellStockComputesCostCorrectlyTest()
    {
        StockDTO dto = new StockDTO( 7, "GOOG" );

        SellStockActivity sellActivity =
            wfRunner.RunSingleActivity<SellStockActivity>( dto );

        double price = testStockSvc.LookupPrice( dto.Ticker );
        int quantity = dto.Quantity;

        Assert.AreEqual( quantity * price, sellActivity.Cost );
    }

    // ...
}

This test method (SellStockComputesCostCorrectlyTest) is remarkable for several reasons:

  1. We are taking a single Activity, not a workflow, and executing it.

  2. We are passing a strongly typed DTO (data transfer object) rather than name/value pairs in a Dictionary<string, object>.

  3. Most Remarkably: We are getting the actual instance of the activity returned to us so that we can explore its properties. Notice that we assert on sellActivity.Cost directly:

    Assert.AreEqual( quantity * price, sellActivity.Cost );

    This is not easy to pull off - WF intentionally hides activities and workflows it creates behind workflow instances (proxies basically). This hack might be bad for production systems, but it *rocks* for unit testing.It means you get intellisense rather than programming against strings in name/value collections.

  4. We are not waiting for the workflow to complete or using the ManualWorkflowSchedule. WfRunner is doing that for us because the method RunSingleActivity is a blocking call.

Just so you don't think I'm trying to pull a fast one: Here's that same listing with all the gory details left in. Notice how we're using WF Services for DI with our test stubs.

[TestClass]
public class WorkflowTests : IDisposable
{
    //
    // Define some objects that will be used across all tests.
    //
    private IAccountService testAccountSvc;
    private IStockService testStockSvc;
    private Account testAccont;
    private WfRunner wfRunner = new WfRunner();

    public WorkflowTests()
    {
        //
        // Initialize common data for all tests.
        // This is basically what the host of the wf-runtime would do
        // but we're using test doubles / stubs for our services.
        //
        Dictionary<string, double> stocks = new Dictionary<string, double>();
        stocks.Add( "goog", 350 );
        stocks.Add( "msft", 25 );

        testAccont = new Account( 1774, 5000 );

        this.testStockSvc = new TestStockService( stocks );
        this.testAccountSvc = new TestAccountLookup( testAccont );

        // Install these services for use by our WF activities.
        wfRunner.AddService( testStockSvc );
        wfRunner.AddService( testAccountSvc );
    }

    [TestMethod]
    public void SellStockComputesCostCorrectlyTest()
    {
        StockDTO dto = new StockDTO( 7, "GOOG" );

        SellStockActivity sellActivity =
            wfRunner.RunSingleActivity<SellStockActivity>( dto );

        double price = testStockSvc.LookupPrice( dto.Ticker );
        int quantity = dto.Quantity;

        Assert.AreEqual( quantity * price, sellActivity.Cost );
    }

    // ...

}

Expected Exceptions

That was pretty awesome huh? We solve several of our problems I identified above (testing single activities, references to the activity, waiting on workflows, and untyped name/value collections as input). The last one to cover is exceptions as success.

When we try to buy a stock and we don't have enough money, the workflow will throw an InsufficientFundsException. This type is a custom exception created as part of my wf application - it's not part of .NET. We want to test for this exception:

[TestMethod]
[ExpectedException(typeof (InsufficientFundsException))]
public void CannotBuyWithInsufficentFundsTest()
{
    ExchangeStocksDTO dto =
        new ExchangeStocksDTO
            {
                AccountID = 1774,
                StockToBuy = "MSFT",
                StockToSell = "GOOG",
                SellQuantity = 5,
                BuyQuantity = 7000
            };

    wfRunner.RunWorkflow<ExchangeStocksWorkflow>( dto );
}

Notice that we're using the ExpectedException attribute. We just call RunWorkflow as a regular method and we get the exception back as a synchronous error. That is fantastic already. But look at the call stack:

SampleLibrary.InsufficientFundsException: Wrapped excpetion message: Insufficient funds for account 1774.
---> SampleLibrary.InsufficientFundsException: Insufficient funds for account 1774.
at SampleLibrary.Account.Withdrawl(Double amount)
at SampleLibrary.DebitAccount.Execute(ActivityExecutionContext ctx)
at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(T activity, ActivityExecutionContext executionContext)
at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(Activity activity, ActivityExecutionContext executionContext)
at System.Workflow.ComponentModel.ActivityExecutorOperation.Run(IWorkflowCoreRuntime workflowCoreRuntime)
at System.Workflow.Runtime.Scheduler.Run()
--- End of inner exception stack trace ---
at Kennedy.WorkflowTesting.WfRunner.TransformAndThrowIfRequired(Exception realException)
at Kennedy.WorkflowTesting.WfRunner.RunWorkflow[T](Dictionary`2 namedArgumentValues)
at Kennedy.WorkflowTesting.WfRunner.RunWorkflow[T](Object workflowDTO)
at SampleLibrary.Tests.WorkflowTests.CannotBuyWithInsufficentFundsTest()

The WfRunner class has determined there was an exception. Rather than just rethrowing it and losing the callstack (notice it's still intact), we do this operation called TransformAndThrowIfRequired. TransformAndThrowIfRequired takes the real exception, uses reflection to recreate another exception of that type and wraps the real exception as the inner exception.

This both preserves the exception type (critical for the ExpectedException behavior) and the callstack (critical for debugging).

Running these tests inside Visual Studio we get all green!

I hope you find this library adds significant value to unit testing of your Windows Workflows. Personally, I think it makes unit testing of your Windows Workflows practical in the real world.

Enjoy!
Michael

Note: This only applies to WF 3.0/3.5. WF 4.0 which is part of .NET 4 which is shipping the end of 2009 may change this considerably.

Tweet this Follow me on Twitter Post this to dotnetshoutout.com Digg this Submit this to Stumbleupon email this post

Test Driven Development, Approval Testing, and a Song - Oh Boy!

Wednesday, January 07, 2009 12:17:54 PM (Pacific Standard Time, UTC-08:00)

So my buddies Dan Gilkerson and Llewellyn Falco have been doing some brilliant, ground breaking work on advancing the state of unit testing and TDD with a concept they call Approval Testing.

To highlight the transition from

unit testing -> TDD -> BDD -> Approval Testing

Dan wrote a parody of the song Let It Be.

The Music Video: Let it BBD


If you like it, then kick it on DotNetKicks.com

Tweet this Follow me on Twitter Post this to dotnetshoutout.com Digg this Submit this to Stumbleupon email this post

Unit Testing Coming to a Workflow Near You

Tuesday, September 30, 2008 7:50:29 AM (Pacific Standard Time, UTC-08:00)
[Update: See the follow up post "Significant Advances in Unit Testing Windows Workflow"]

If you've been working with Windows Workflow, you'll find it has some cool features for orchestration, long running operations, state machines, etc.

However you won't find very much support for Test Driven Development (TDD) or unit testing in general. In fact the architecture that makes Windows Workflow powerful (strict separation of workflow, activities, and the host for example) really gets in the way of unit tests.

There has been some work done on unit testing Windows Workflows. Here's some links:

These are all very creative solutions. But, personally I find all of them more complex than they need to be. So in the near future I'll be putting together some libraries and samples on unit testing Windows Workflow. I think you'll find them far more powerful and at the same time simpler than anything out there.

So until I get that finished, if you have any feedback or considerations on unit testing Windows Workflow I'd love to hear it. If there are other articles I'm missing, please post them in the comments.

I think you're going to like this…



kick it on DotNetKicks.com

Tweet this Follow me on Twitter Post this to dotnetshoutout.com Digg this Submit this to Stumbleupon email this post


Just a site note: I'm doing my part to rid the world of IE 6. Visit this site with IE 6 and you'll get a shameful message telling you to "Stop Living in the Past".