LightBDD 3.x logo

Welcome LightBDD 3.0


I am happy to announce the LightBDD 3.0!

Before I go to the post details, here are some quick links for those who just want to start looking into it:

In LightBDD project, I am trying to follow semantic versioning guidelines. I was able to develop the LightBDD 2.x series for almost 2 years without introducing a major breaking changes.
It means however that more and more methods and classes become obsolete during the project evolution, making the code base growing bigger and more difficult to maintain. Over that time, I observed also that some of the design decisions introduced unnecessary complexity or confusion. Finally, with the recent request to provide a signed version of the packages (which is a big breaking change), I have decided that it’s the time to make LightBDD 3.x.

So what’s new?

As I mentioned above, the main driver of going to version 3.0 is the introduction of few breaking changes, but from the usage perspective there is actually not that many changes as people might be expected. In this sense, the LightBDD project is taking an evolution path not the rewrite one 🙂

What’s new then?

No more confusing scenario namespaces

I guess the biggest usability improvement is that all the different scenario styles are located now in one namespace LightBDD.Framework.Scenarios.
No matter of what scenario style is your favourite dear reader, you can just specify using LightBDD.Framework.Scenarios; in your code to get access to all the extension methods on the Runner property.

A similar simplifications have been made as well in the other areas of LightBDD.

Fluent scenarios are first-class citizens now

In LightBDD 2.x, scenarios could be built in fluent way by calling Runner.NewScenario() method (and using proper namespace). While it was not difficult to do, such boilerplate code had to be repeated in every scenario using fluent syntax.

In LightBDD 3.0, the NewScenario() method has been removed, and it is possible now to start composing scenario in fluent manner by using Runner directly:

await Runner

Removed obsolete code and simplified internals

With LightBDD 3.0 release, over 5000 lines of code has been deleted!

Besides obsolete code removal and many simplifications in the internal code, there are two visible areas of LightBDD that have been removed:

  • LightBDD.NUnit2 integration has been removed in favor of LightBDD.NUnit3,
  • Runner.RunScenarioActionsAsync() methods have been removed in favor of easier fluent scenario integration.

Signed assemblies

The last, a bit less visible but significant change is that all the packages except LightBDD.Fixie2 are signed now.
It means that LightBDD dlls can be put to GAC now, but more importantly, LightBDD can be used now to test signed test projects.

Why is that change a breaking one then?
Well, the signed assemblies are not binary compatible with non-signed ones. More over, the binding redirection will not work for them neither, which means that all projects using LightBDD have to be updated to use LightBDD 3.x and recompiled.

Lessons learned

Working on LightBDD gives me a lot of satisfaction. First of all, it is amazing to see people using it and finding the project useful. The second bit I love is that I learn on how to efficiently maintain library code.

When worked on LightBDD 2.0, I designed it to be modular, with clear separation of the engine (LightBDD.Core), the framework (LightBDD.Framework) and all the integrations (LightBDD.NUnit3 etc).
Having this separation in place, helped me to create solution that is easily extendable, due to proper separation of concerns. As example, I could easily introduce LightBDD.Fixie2 integration or add compact scenario syntax.
However, such separation of projects uncovered poor design decisions as well…

Using interfaces vs abstract classes

The LightBDD 2.x relies heavily on interfaces to expose only crucial information between LightBDD.Core and Framework. Going with interface approach allowed easy extension of the framework without exposing too much of the internals, allowing to refactor them easily as needed.

Such approach lead however to interesting observations, that depending on who is providing implementations of the interface, the architecture is or is not extendable.

TL;DR: Use interface when providing API together with implementation to the user. Use abstract classes when expecting user to provide the implementation.

Let’s take a look at two examples.

Example 1

The LightBDD.Core exposes following interfaces:

public interface IStepDecorator
    Task ExecuteAsync(IStep step, Func<task> stepInvocation);

The IStepDecorator allows to define a decorator and configure LightBDD to call it for step methods.

The implementer of the decorator can use IStep interface to access step details:

public interface IStep
    IStepInfo Info { get; }
    void Comment(string comment);

The code presented above is the original LightBDD 2.x version of that interface.

During the LightBDD 2.x evolution, there was a need to expose more information about the step. As the implementation of that interface was provided by the LightBDD.Core itself, not the users, it was safe to just add more members to the interface, ending up with the latest version:

public interface IStep : IExecutable
    IStepInfo Info { get; }
    void Comment(string comment);
    IDependencyResolver DependencyResolver { get; }
    object Context { get; }

This change was easy to ship to the users without worries about backward compatibility of LightBDD packages.

Example 2

The IIntegrationContext is an interface defined in LightBDD.Core that has to be implemented by every integration project such as LightBDD.NUnit3.

public interface IIntegrationContext
    IMetadataProvider MetadataProvider { get; }
    INameFormatter NameFormatter { get; }
    Func<Exception, ExecutionStatus> ExceptionToStatusMapper { get; }
    IFeatureProgressNotifier FeatureProgressNotifier { get; }
    Func<object, IScenarioProgressNotifier> ScenarioProgressNotifierProvider { get; }
    IExecutionExtensions ExecutionExtensions { get; }

Over time, it appeared that a new feature has to be added to return IDependencyContainer DependencyContainer { get; } property.
Unfortunately, adding this property to the interface will be a breaking change, as now, all of the implementers will be providing not full implementation of the interface.

Of course, all LightBDD implementations would get updated at the same time, but backward incompatibility could still pop-up to the user world, if users would only upgrade the LightBDD.Core package but no others.

So, how did it get implemented?
Actually, the interface was left unchanged, but instead, the abstract IntegrationContext class was updated with the new property:

public abstract class IntegrationContext : IIntegrationContext
    public abstract IMetadataProvider MetadataProvider { get; }
    public abstract INameFormatter NameFormatter { get; }
    public abstract Func<Exception, ExecutionStatus> ExceptionToStatusMapper { get; }
    public abstract IFeatureProgressNotifier FeatureProgressNotifier { get; }
    public abstract Func<object, IScenarioProgressNotifier> ScenarioProgressNotifierProvider { get; }
    public abstract IExecutionExtensions ExecutionExtensions { get; }
    public abstract LightBddConfiguration Configuration { get; }
    public virtual IDependencyContainer DependencyContainer { get; } = new BasicDependencyContainer();

It is worth to note, that the new property is virtual and contains the default value, in case it’s not overridden yet. This way, it is safe to update LightBDD.Core package and still successfully use it with older versions of the LightBDD.Framework and integrations.

The final note here is that in LightBDD 3.0, there is no more IIntegrationContext interface and IntegrationContext is now the base type for all contexts.

Extension methods

The extension methods are widely used concept in LightBDD.
They are used for extending IBddRunner with new scenario writing styles, as well as, for working with LightBDD configuration.

While this mechanism worked very well in LightBDD 2.x, there was one flaw in the design.

There is nothing more annoying than seeing such code working very well in one source file:

public partial class Basket_feature : FeatureFixture
    public void Adding_items_to_the_basket()

… but failing in other file with errors like:

Error CS1503 Argument 2: cannot convert from 'method group' to 

Such problems were happening often when wrong usings were specified in the source file. While using LightBDD.Framework.Scenarios.Basic; will make the code compiling,
the using LightBDD.Framework.Scenarios.Extended; will fail in this case.

In LightBDD 3.0, all the namespaces have been simplified, and now all the flavors of the scenario styles are located in one namespace: using LightBDD.Framework.Scenarios;.

No more confusion!