Fluent API

BDDfy can scan your tests in one of two ways: using Reflective API and Fluent API. Reflective API uses some hints to scan your classes. These hints are provided through Method Name Conventions and/or [ExecutableAttribute][/BDDfy/executable-attributes.html] which we have discussed before. For this post we will concentrate on Fluent API.

I just thought I would share a bit of history with you first. I had just released BDDfy V0.5 and the API had kinda settled. So I thought I'd write an introductory article on CodeProject to promote the framework. On the top of article I said 'BDDfy is very extensible. In fact, BDDfy core barely has any logic in it. It delegates all its responsibilities to its extensions'. Then I thought that just claiming a framework is extensible does not mean anything if I cannot provide a sample for it. That is why I wrote the Fluent API as I was writing that article to prove it to myself that BDDfy is highly extensible and also to provide an example of that. Well, I started it as an extensibility example; but then I liked and felt the need for it and baked into the framework. Today it is no longer a sample and in fact it is even more popular than Reflective mode!!

Fluent API

Fluent API of BDDfy does not really require much explanation as it is quite fluent ;-) So instead of trying to explain to you how it works I will just provide an example.

In the Method Name Conventions post I wrote a scenario called 'BDDfyRocks' which I repeat here for your convenience:

public class BDDfyRocks
{
    [Test]
    public void ShouldBeAbleToBDDfyMyTestsVeryEasily()
    {
        this.BDDfy();
    }

    void GivenIHaveNotUsedBDDfyBefore()
    {
    }

    void WhenIAmIntroducedToTheFramework()
    {
    }

    void ThenILikeItAndStartUsingIt()
    {
    }
}

And then we expanded that scenario to the second one shown below:

public class BDDfyRocksEvenForBddNewbies
{
    [Test]
    public void ShouldBeAbleToBDDfyMyTestsVeryEasily()
    {
        this.BDDfy();
    }

    void GivenIAmNewToBdd()
    {
    }

    void AndGivenIHaveNotUsedBDDfyBefore()
    {
    }

    void WhenIAmIntroducedToTheFramework()
    {
    }

    void ThenILikeItAndStartUsingIt()
    {
    }

    void AndILearnBddThroughBDDfy()
    {
    }
}

Let's rewrite these two scenarios using Fluent API:

using NUnit.Framework;

namespace BDDfy.FluentApi
{
    public class BDDfySeriouslyRocks
    {
        [Test]
        public void BDDfyRocks()
        {
            this.Given(_ => GivenIHaveNotUsedBDDfyBefore())
                .When(_ => WhenIAmIntroducedToTheFramework())
                .Then(_ => ThenILikeItAndStartUsingIt())
                .BDDfy();
        }

        [Test]
        public void BDDfyEvenRocksForBddNewbies()
        {
            this.Given(_ => GivenIAmNewToBdd())
                    .And(_ => AndIHaveNotUsedBDDfyBefore())
                .When(_ => WhenIAmIntroducedToTheFramework())
                .Then(_ => ThenILikeItAndStartUsingIt())
                    .And(_ => AndILearnBddThroughBDDfy())
                .BDDfy();
        }

        void GivenIHaveNotUsedBDDfyBefore()
        {
        }

        void GivenIAmNewToBdd()
        {
        }

        void AndIHaveNotUsedBDDfyBefore()
        {
        }

        void WhenIAmIntroducedToTheFramework()
        {
        }

        void ThenILikeItAndStartUsingIt()
        {
        }

        void AndILearnBddThroughBDDfy()
        {
        }
    }
}

This class has two test methods each representing one of the scenarios. The reports generated by these tests are exactly the same as those shown in the Method Name Conventions post.

There are a few important differences in implementation as follows:

  • In Reflective API the only thing you need to call is this.BDDfy(); or one of its overloads that accepts the story type argument and/or the custom scenario title (and then BDDfy will find your steps using conventions or attributes). In the Fluent API you should explicitly specify all your steps before calling the BDDfy method.
  • When using Reflective API the scenario name is driven by the name of the class because each class represents a scenario. In Fluent API, however, a class usually represents a story (or collection of related scenarios in the absence of a story) and scenarios are represented by methods. That is why while porting the sample to use Fluent API I renamed my scenario method names to match the class name that represented the scenario in the source sample. This is to ensure that I will get the same title for my scenarios after using Fluent API.
  • In Reflective API BDDfy would pick up any combination of scenario steps by method name conventions and ExecutableAttribute; but in Fluent API mode you are in complete control. This means that regardless of what your method names are or whether they are decorated with ExecutableAttribute or not the steps you specify using the Fluent API will run by BDDfy. Likewise if there is a method that complies with method name conventions and/or is decorated by ExecutableAttribute (or one of its derivatives) but is not specified in your Fluent API call it is not going to be picked up by the framework. Reflective and Fluent modes run in isolation of each other and you choose the mode by the way you call the BDDfy method.
  • In Reflective mode the method name starting with 'AndGiven' and 'AndWhen' will result into steps starting with 'And': the framework knows that you have provided the extra 'Given' and 'When' words only to comply with its conventions and as such drops them from the reports. In the Fluent API your step titles are derived directly from your method name. So when porting the example from using Method Name Convention to Fluent API I renamed AndGivenIHaveNotUsedBDDfyBefore to AndIHaveNotUsedBDDfyBefore to avoid getting 'And given' in my report.
  • You notice that I removed two methods while porting the code to use Fluent API: WhenIAmIntroducedToTheFramework and ThenILikeItAndStartUsingIt. These two methods were repeated in each scenario; but I ported all scenarios and methods to the same class; so we can avoid duplication. Well, in all fairness the same could be achieved in the Reflective mode through inheritance where the shared logic lives in a base class that other scenarios subclass; but I think the reuse is kinda more natural in the Fluent mode.
  • If you use R#, in Reflective mode if you write your steps as private methods you are going to get R# warning for unused methods because R# does not have any idea about the reflection magic going behind the scenes. Using Fluent API because you explicitly call the methods you no longer get the R# warning because you are using the methods. In order to avoid the warning in Reflective mode you may define your methods as protected or public to avoid the warnings.

Adding Story

Out of the box, there is only one way to specify your Story and to associate it with scenarios and that is using StoryAttribute. This is the same for Reflective and Fluent modes.

Let's add story to the above example:

using BDDfy.Core;
using NUnit.Framework;

namespace BDDfy.FluentApi
{
     [Story(
        AsA = "As a .net programmer",
        IWant = "I want to use BDDfy",
        SoThat = "So that BDD becomes easy and fun")]
    public class BDDfySeriouslyRocks
    {
        [Test]
        public void BDDfyRocks()
        {
            this.Given(_ => GivenIHaveNotUsedBDDfyBefore())
                .When(_ => WhenIAmIntroducedToTheFramework())
                .Then(_ => ThenILikeItAndStartUsingIt())
                .BDDfy();
        }

        // The rest is removed for brevity
    }
}

As mentioned in a previous post, to create a story you need to decorate a class with StoryAttribute. In the Fluent mode the story class is usually the same as the class that contains the scenarios because, unlike Reflective mode, a scenario does not necessarily map to a class and is usually implemented in a method.

This of course has its pros and cons. The nice thing about this approach is that you can see an entire story in one file/class; at the same time that could be considered a disadvantage because some stories are rather big and have quite a few scenarios which will result into a big class. Again you do not have to put all the scenarios of a story in one class: that is just one option.

Running the tests now will include the story title and narrative into console and html reports.

FAQ

These are some of the FAQs I have received for Fluent API:

Should I have my methods in the right order?

In the Reflective mode there is a situation where you have to put your methods in the right order and that is when you have more than one 'AndGiven' or 'AndWhen' or 'And' in which you case the 'and' parts are executed in the order they appear in the class. In the Fluent API that does not matter. The methods are executed in the order specified using the Fluent API. So it does not matter in what order they appear in the class.

How I can reuse some of the testing logic?

As mentioned above with Fluent API it is very easy to reuse the test logic across all scenarios of the same story because usually they are all in the same class. If the logic is not in the same class, you can still use inheritance or composition to compose a scenario.

Can my step methods be static or should they be instance methods?

BDDfy handles both cases. So feel free to use whatever makes sense.

comments powered by Disqus