Skip to content

Specify is an opinionated .Net Core testing library that builds on top of BDDfy from TestStack

License

Notifications You must be signed in to change notification settings

mwhelan/Specify

Repository files navigation

Specify

What is it?

Specify is a .Net testing library that builds on top of BDDfy from TestStack. While BDDfy is primarily intended for BDD testing, it is beautifully designed to be very easy to customize and extend. Specify provides a base test fixture that extends BDDfy with additional features, such as automatic creation of the SUT (System Under Test), using auto-mocking or your IoC container, and logging with your logging framework.

When I first started using BDDfy for acceptance testing, I would use a different framework for unit testing, but I didn't like the context switching between different frameworks, syntaxes and testing styles. Specify provides a base fixture which gives a consistent experience for all types of tests (or specifications). Why not have the fantastic BDDfy reports for all of your different test types?

Build Status Documentation Status NuGet (stable) NuGet (latest)

Overview of Features

  • Tests use a context-specification style, with a class per scenario.
  • SUT factory with pluggable auto-mocking or Ioc containers. There is transparent built-in support for NSubstitute, Moq, and FakeItEasy.
  • Tests can be resolved from your IoC container. There is built-in support for the Autofac container.
  • BDDfy Reports are produced for all of your test types
  • Specify uses LibLog for logging, a logging abstraction which provides support for NLog, Log4Net, EntLib Logging, Serilog and Loupe, and allows your users to define a custom provider if necessary.

Context-Specification Style

With context-specification you have a class per scenario, with each step having its own method and state being shared between methods in fields. This means that the setup and execution only happen once (the context), and then each Then method is a specification that asserts against the result of the execution.

Specify provides two generic base classes that your test class can inherit from:

  • ScenarioFor<TSut> is for low level specifications, such as unit and integration tests, and would normally be used with an auto-mocking container. Reports show Specifications For: [SUT Name] instead of a user story.
  • ScenarioFor<TSut, TStory> is for higher level specifications, such as acceptance tests. These are the typical BDDfy user story tests and would use an IoC container for the SUT factory. Reports show the user story or business value story.

These classes follow the Given When Then syntax (though there is nothing to stop you from customizing BDDfy to use a different syntax if you want).

public class DetailsForExistingStudent : ScenarioFor<StudentController>
{
    ViewResult _result;
    private Student _student = new Student { ID = 1 };

    public void Given_an_existing_student()
    {
        Container.Get<ISchoolRepository>()
            .FindStudentById(_student.ID)
            .Returns(_student);
    }

    public void When_the_details_are_requested_for_that_Student()
    {
        _result = SUT.Details(_student.ID) as ViewResult;
    }

    public void Then_the_details_view_is_displayed()
    {
        _result.ViewName.Should().Be(string.Empty);
    }

    public void AndThen_the_details_are_of_the_requested_student()
    {
        _result.Model.Should().Be(_student);
    }
}

Which will show the following report, customized to show Specifications For: StudentController

BDDfy report

System Under Test

The TSut type parameter represents the System Under Test, which is the class that is being tested. Each class has a SUT property, which is instantiated for you by the auto-mocking or IoC container. You can override this too, if you want.

User Story or Value Story

The TStory type parameter represents the user story. Specify provides two Story classes, but BDDfy lets you create additional formats if you wish:

  • User Story: The original user story, in the form As a <type of user> I want <some functionality> so that <some benefit>.
  • Value Story: The business value story, with the emphasis on the business value. In order to <achieve some value>, as a <type of user>, I want <some functionality>.

SUT Factory

Specify can create the SUT for you, using either an auto-mocking container or an IoC container as a SUT factory. This is configurable, on a per test assembly basis. A fresh new container is provided for each specification, so you can change the configuration for each test without impacting other tests. Most IoC containers provide this child container functionality (though they might call it by different names).

The Specification classes allow you to interact with the SutFactory via a Container property. The Get methods allow you to retrieve SUT dependencies. The Register methods allow you to provide implementations that will be used in the creation of the SUT. You can also set the SUT directly if you need to override the creation for a particular scenario. The SUT is lazily created the first time it is requested, so registering types and setting the SUT need to happen before the first request to the SUT property.

Auto-Mocking and IoC Adapters

There is transparent built-in support for NSubstitute, Moq, and FakeItEasy auto-mocking containers. Just add a reference to one of these projects and Specify will detect it and use the relevant adapter.

If no mocking framework is referenced then Specify will default to an Autofac-based IoC container. Just add an Autofac module, which Specify will automatically detect and register your dependencies.

Alternatively, to use a particular mocking framework or IoC container in your tests you just have to implement the Specify IContainer interface.

The containers are largely based on Chill's containers, by Erwin van der Valk. Chill is a great framework, which I recommend you check out.

Test Lifecycle

Specify uses BDDfy's Reflective API to scan its classes for methods. By default, BDDfy recognises the standard BDD methods, as well as Setup and TearDown. You can read more about them here and you can always customize them if you have your own preferences by creating a new BDDfy MethodNameStepScanner. The method name:

  • Ending with Context is considered as a setup method (not reported).
  • Setup is considered as as setup method (not reported).
  • Starting with Given is considered as a setup method (reported).
  • Starting with AndGiven is considered as a setup method that runs after Context, Setup and Given steps (reported).
  • Starting with When is considered as a transition method (reported).
  • Starting with AndWhen is considered as a transition method that runs after When steps (reported).
  • Starting with Then is considered as an asserting method (reported).
  • Starting with And is considered as an asserting method (reported).
  • Starting with TearDown is considered as a finally method which is run after all the other steps (not reported).

Logging

Specify uses LibLog for logging. You can turn logging on by setting the LoggingEnabled property to true on your SpecifyConfig file (it's off by default). LibLog is a logging abstraction

LibLog contains transparent built-in support for NLog, Log4Net, EntLib Logging, Serilog and Loupe, and allows your users to define a custom provider if necessary.

Specify logs every method of the scenario and its duration, and every exception. You are also free to make your own logging calls.

public class MyClass
{
    private static readonly ILog Logger = LogProvider.For<MyClass>(); 

    public MyClass()
    {
        using(LogProvider.OpenNestedContext("message"))
        using(LogProvider.OpenMappedContext("key", "value"))
        {
            Logger.Info(....);
        }
    }
}

Running the tests

One of the things I've always liked about this class per scenario approach is not having test framework attributes, such as [Test] and [Fact] on all of my test methods. Once you put these attributes on the base class the inheriting classes don't need them - all the main test frameworks are clever enough to discover them on base classes. This poses a bit of a challenge with a library like this. Thankfully, newer frameworks, like Fixie, don't force you to use attributes for test discoverability. Just use this Fixie convention (available in the Specify.Examples project) to run Specify tests:

public class FixieSpecifyConvention : Convention
{
    public FixieSpecifyConvention()
    {
        Classes
            .Where(type => type.IsUnitScenario() || type.IsStoryScenario());

        Methods
            .Where(method => method.Name == "Specify");
    }
}

Unfortunately, even the newer versions of xUnit and NUnit seem to still require attributes (please let me know if I'm wrong on this score). Again, I've gone for the copy/paste solution. Just create a base class to extend the two Specify classes. Here is an example with NUnit. I've adopted the convention of prefixing the class with the letter of the test framework so as to remove any ambiguity between the two:

[TestFixture]
public abstract class ScenarioFor<TSut, TStory> : Specify.ScenarioFor<TSut, TStory>
    where TSut : class
    where TStory : Story, new()
{
    [Test]
    public override void Specify()
    {
        base.Specify();
    }
}

[TestFixture]
public abstract class ScenarioFor<TSut> : Specify.ScenarioFor<TSut> 
    where TSut : class
{
    [Test]
    public override void Specify()
    {
        base.Specify();
    }
}

Configuration

Just create a class in your test assembly that inherits from the SpecifyConfiguration class. The main thing to configure is the container that the SUT factory will use:

public class SpecifyConfig : SpecifyBootstrapper
{
    public override IApplicationContainer CreateApplicationContainer()
    {
        return new AutofacNSubstituteContainer();
    }
}

Unit Tests

You can read more about the unit testing approach here:

About

Specify is an opinionated .Net Core testing library that builds on top of BDDfy from TestStack

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages