Storyteller 4.2.0


Next

Extensions

Previous

Grammars

Managing State during Execution Edit on GitHub


While many if not most specifications will be authored with the grammars in a single Fixture, it is very possible and frequently desirable to use multiple Fixture sections within a specification body. Fortunately, Storyteller has a mechanism in Fixtures to store and retrieve object state within the execution of a specification.

Using Fixture.Context.State

The Fixture class exposes a state bag reachable by Fixture.Context.State with the signature below:


public interface IState
{
    void Store<T>(T value);
    void Store<T>(string key, T value);
    T Retrieve<T>();
    T Retrieve<T>(string key);
    T RetrieveOrAdd<T>(Func<T> missing);


    T TryRetrieve<T>(string key);
    T TryRetrieve<T>();
}

The Context property on Fixture is available during specification execution and refers to the currently executing specification. This object is shared by all Fixture objects being used during the execution of a specification.

The State object bag can store and retrieve data by either type or by the combination of type and a string name.

An Example of Passing State Between Fixtures

Here is a typical usage of the state bag. Let's say that you are working on an invoicing application where your invoices are made up of many details that are themselves complicated.


public class Invoice
{
    public readonly IList<InvoiceDetail> Details
        = new List<InvoiceDetail>();
}

Pretend that the class below is much more complicated than it really is ;-).


public class InvoiceDetail
{
    public double Amount { get; set; }
    public DateTime Date { get; set; }
    public string Name { get; set; }
    public string Part { get; set; }
}

With Storyteller's mantra of self-contained specifications in mind, you need some Fixtures to construct new Invoice and InvoiceDetail objects. If an InvoiceDetail was sufficiently complicated, I would probably choose to:

  1. Create a Fixture completely dedicated to creating a single detail
  2. Create a Fixture to set up the state of a single Invoice
  3. Use the invoice detail Fixture as an embedded section within a Fixture for the Invoice setup.

The missing piece of the list above is how to attach the InvoiceDetail objects created inside the embedded section to the new Invoice object created by the parent Fixture, and that is where the Context.State becomes useful.

First, here's what the Fixture for the parent Invoice setup might look like:


[Hidden]
public class InvoiceFixture : Fixture
{
    public override void SetUp()
    {
        // The ISpecContext *is* available during SetUp()
        Context.State.Store(new Invoice());
    }

    public IGrammar AddDetail()
    {
        return Embed<InvoiceDetailFixture>("with invoice detail:");
    }
}

Notice how it creates a new Invoice object in its SetUp() method and stores that in the contextual state? The next piece is the Fixture to set up a detail:


[Hidden]
public class InvoiceDetailFixture : Fixture
{
    public void TheDetailIs(double amount, DateTime date, string name, string part)
    {
        // Build a new detail
        var detail = new InvoiceDetail
        {
            Amount = amount,
            Date = date,
            Name = name,
            Part = part
        };

        // Add the new detail to the current Invoice
        Context.State.Retrieve<Invoice>().Details.Add(detail);
    }

    // And many more grammars for all the optional properties of a
    // real world invoice detail
}

An InvoiceDetail object is created in the TheDetailIs grammar method and immediately added to the Invoice object retrieved from contextual state that was put there by the InvoiceFixture in its SetUp() method.

The "Current Object"

Some of the built in grammar types in Storyteller quietly depend on a special property slot that is reachable from any Fixture class as Fixture.Context.State.CurrentObject, or a shorthand CurrentObject that gets to the same data.


[FormatAs("When the system receives a new invoice")]
public void WithNewInvoice()
{
    // Other grammars, built in grammars,
    // and other fixtures can now share and
    // use *this* particular Invoice object
    CurrentObject = new Invoice();

    // The code above is just shorthand for this below:
    // Context.State.CurrentObject = new Invoice();
}