Wednesday, May 06, 2009

Why BDD? Can it help me?

I've been watching a lot of people arround me and on the internet talking about BDD. Of course as alwars, some were more extremist and said that TDD is dead, and BDD is much better. 

Why is BDD needed, in general?

Most people develop code, and design, mainly on the simple tasks, by exploration. They write some code, see if it works, refactor it, make it better, then if it still doesn't work they sit down and think of a better solution. After this 3rd stage the real thinking is needed. This fact comes from human nature and the fact that most of us like to do things with the minimum effort required. Unfortunately, writing tests first, moves the real thinking phase ahead, thus making it more unconfortable, because the first thing you need to do is to think how to design the test that will design your code. This isn't as simple as just starting to type code then seeing if it works. So, conslusion one would be that writing tests first, is harder, because it doen't allow the people to warm up, before being sent onto the pitch. 

On the other hand, it seems that there are still lots of people that do not make a clear difference between writing automated tests and Test Driven Development, where the tests are written first, and actually drive the way you're developing the code, making it usually smaller, and less dependent, as a general rule better. This fact has been noticed by some incredibly smart people, and they said maybe TDD is too abstract, and we need a simpler way to show people the philosophy behind it, so we need to simplify it and enforce somehow people to write the tests first, and let the the tests drive the way the code is written. So the second concusion is that, maybe TDD should be simplified, renamed, handed out to the masses, as Dan North (original developer of BDD concept) said:

I had a problem. While using and teaching agile practices like test-driven development (TDD) on projects in different environments, I kept coming across the same confusion and misunderstandings. Programmers wanted to know where to start, what to test and what not to test, how much to test in one go, what to call their tests, and how to understand why a test fails.

Why do I need it?

After practicing TDD for many years, I saw one drawback, that kept coming to me: in time, I forgot what they did, and sometimes I needed lots of time to understand them again. Unfortunately for this there isn't a solution:

 - small easy to understand unit test: If you develop lots of small unit tests, lots means that you have to read a lot of them to understand what is there they test. The problem of quantity
 - test names very explicit: well, even this sometimes becomes a mess, especially with integration or functional tests, as the names become longer and longer (TestMaskMoveRotateZoomWithUndo() or TestAddMoveRemovePointOnMaskAndCheckDimentionsOnPanelWithUndo() )
 - comments: just like any other piece of code, tests need comments, and they seem to be the best solution to understand what they do. But just like code, people either forget about them, write something just to be there, or forget to update the comments when they update the tests or copy paste.
 - lack of clear flow in tests: preparation, action and verification phases: any good test has 3 steps. One where the context is prepared, then when something is done, and the third is the verification phase. However, there are tests where these 3 aren't easy to spot, and with integration/functional tests especially, because they tend to take time, there are many cases where some of the steps are repeated (add/edit/delete tests for instance, where there is a preparation phase, then action add, verify, action edit, verify, action delete verify). This makes the tests harder to read.

So what do I do?

It seems that BDD tends to solve all the above problems, changing the naming and making test first easier to understand, and my problem by making tests easier to be understood later. It is not perfect, but definately helps. 

But BDD is not just this, BDD is a lot more. BDD for the first time, allows or simplifies the real implementation of the acceptance tests. If you use XP, and you have stories, and for each you define acceptance tests with the customer (see sample http://danbunea.blogspot.com/2008/04/chapter-3-communicating-and.html), which means that both you and him understand them, because they use english and not c, java, ruby or c#, BDD allows you to write them in code very close to the way they are written on the story card. Now this is a breakthough:

Story: import images

The customer will be able to import and process images using the API. 

Acceptance test X:

Given an image over 20Mb
When the user imports it
Then the program should reject it for being too big

would be in code:

   [Test]
  public void ShouldntImportOver20Mb()
  {
        Image image = null;
        ImportResult result = null;
       "Given an image over 20Mb".Given(
         () =>
         {
                image = new Image("largeImage.jpg");
         });


     "When the user imports it".When(
        () =>
        {
             result = Importer.Import(image);
         });


     "Then the program should reject it for being too big".Then(
       () =>
       {
           Assert.Equal("Image is too big, over 20Mb", result.Message);
       });
  }

Now this isn't any fancy BDD framework, but a simple string Extention methods, proposed by Mihai Lazar (brilliantly simple idea), which despite the simplicity of the test, allows me to read easily what is going on, see clearly the flow and see the real correspondence with the acceptance test in the story. 

Conclusion

The example above is too simple, to allow real advantages over TDD, because as a unit test it would be:

   [Test]
  public void ShouldntImportOver20Mb()
  {
           Image  image = new Image("largeImage.jpg");
            ImportResult result = Importer.Import(image);
           Assert.Equal("Image is too big, over 20Mb", result.Message);
  }

which is fairly simple to understand, but what if it were more complicated:


  [Test]
  public void Test2AddMoveRemovePointOnMaskAndPanelDimentionsWithUndo()
  {
  ViewShape view = DragDropShape();
  ShapeEditorControl ctrl = (ShapeEditorControl)view.FindName("shapeEditor");



  int pointsBefore = ctrl.CurrentOption.Points[0].Count;

  //simulate adding new point near to first point
  Point newPoint = new Point(ctrl.CurrentOption.Points[0][0].X + 1, ctrl.CurrentOption.Points[0][0].Y + 5);
  ctrl.InsertThumb(newPoint);


  Assert.AreEqual(pointsBefore + 1, ctrl.CurrentOption.Points[0].Count);
  Assert.AreEqual(1, ctrl.CurrentOption.UndoActionsCount, "added one undo action");

  //move
  Point pointBefore = new Point(ctrl.CurrentOption.Points[0][0].X, ctrl.CurrentOption.Points[0][0].Y);
  Thumb thumb = ctrl.Thumbs[0];
  thumb.RaiseEvent(new DragDeltaEventArgs(3, 3));
  thumb.RaiseEvent(new DragCompletedEventArgs(1, 1, false));

  Assert.AreEqual(pointBefore.X + 3, ctrl.CurrentOption.Points[0][0].X);
  Assert.AreEqual(pointBefore.Y + 3, ctrl.CurrentOption.Points[0][0].Y);
  Assert.AreEqual(2, ctrl.CurrentOption.UndoActionsCount, "added two undo actions");

  ctrl.IsTestMode = true;
  //delete
  MouseButtonEventArgs mbea = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left);
  mbea.RoutedEvent = Mouse.MouseDownEvent;
  ctrl.thumb_MouseDoubleClick(thumb, mbea);

  Assert.AreEqual(pointsBefore, ctrl.CurrentOption.Points[0].Count);
  Assert.AreEqual(3, ctrl.CurrentOption.UndoActionsCount, "added 3 undo actions");


  //Undo
  ClickUndo(view);
  Assert.AreEqual(pointsBefore + 1, ctrl.CurrentOption.Points[0].Count);
  Assert.AreEqual(2, ctrl.CurrentOption.UndoActionsCount, "undone delete");

  //undo
  ClickUndo(view);
  Assert.AreEqual(pointBefore.X, ctrl.CurrentOption.Points[0][0].X);
  Assert.AreEqual(pointBefore.Y, ctrl.CurrentOption.Points[0][0].Y);
  Assert.AreEqual(1, ctrl.CurrentOption.UndoActionsCount, "undone move");

  //undo
  ClickUndo(view);
  Assert.AreEqual(pointsBefore, ctrl.CurrentOption.Points[0].Count);
  Assert.AreEqual(0, ctrl.CurrentOption.UndoActionsCount, "undone add");

  }


and now:

  [Test]
  public void Test2AddMoveRemovePointOnMaskAndPanelDimentionsWithUndo()
  {
     ViewShape view = null;
     ShapeEditorControl ctrl = null;
     int pointsBefore = -1;


     "Given the countour editor open for an image ".Given(
       () =>
       {
                   view = DragDropShape();
                   ctrl = (ShapeEditorControl)view.FindName("shapeEditor");

                 
  int pointsBefore = ctrl.CurrentOption.Points[0].Count;

       });


     "Adding a new point".When(
       () =>
       {
              Point newPoint = new Point(ctrl.CurrentOption.Points[0][0].X + 1, ctrl.CurrentOption.Points[0][0].Y + 5);
              ctrl.InsertThumb(newPoint);

       });

     "should have one more point, and possibility to undo".Then(
       () =>
       {
               Assert.AreEqual(pointsBefore + 1, ctrl.CurrentOption.Points[0].Count);
               Assert.AreEqual(1, ctrl.CurrentOption.UndoActionsCount, "added one undo action");
       });

     Point pointBefore=null;
     "and having a point".When(
       () =>
       {
              pointBefore = new Point(ctrl.CurrentOption.Points[0][0].X, ctrl.CurrentOption.Points[0][0].Y);
       });


     "when moving it".When(
       () =>
       { 
                 Thumb thumb = ctrl.Thumbs[0];
                  thumb.RaiseEvent(new DragDeltaEventArgs(3, 3));
                  thumb.RaiseEvent(new DragCompletedEventArgs(1, 1, false));
       });

     "should be in another position".Then(
       () =>
       {  //move
              Assert.AreEqual(pointBefore.X + 3, ctrl.CurrentOption.Points[0][0].X);
              Assert.AreEqual(pointBefore.Y + 3, ctrl.CurrentOption.Points[0][0].Y);
              Assert.AreEqual(2, ctrl.CurrentOption.UndoActionsCount, "added two undo actions");    
       });

     "when deleting it".When(
       () =>
       { 
              ....
        });
     "should be gone".Then(
       () =>
       { 
              ....
        });

     "clicking undo".When(
       () =>
       { 
              ....
        });
     "should have the deleted point back".Then(
       () =>
       { 
              ....
        });

     "re-clicking undo".When(
       () =>
       { 
              ....
        });
     "should move the point back at initial position".Then(
       () =>
       { 
              ....
        });

     "clicking undo".When(
       () =>
       { 
              ....
        });
     "should get rid of the added point".Then(
       () =>
       { 
              ....
        });


  }

which now looks like a scenario, making it simpler to follow and understand how the application  should behave in this case.

It seems, as a final word, that the first and most productive property of BDD, is that the description of the behavior of the application suddently is in english, and could be read (if you just output the strings on the console) even by the customer. A great leap forward, at least for me.