Fixture classesFixture objects is a breaking change in 3.0 from earlier versions. This was done to conceptually untangle state management from any running application container. 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.
State object that implement the IDisposable interface will be disposed by Storyteller at the end of executing the specification. 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:
- Create a
Fixturecompletely dedicated to creating a single detail - Create a
Fixtureto set up the state of a singleInvoice - Use the invoice detail
Fixtureas an embedded section within aFixturefor theInvoicesetup.
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();
}