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
Fixture
completely dedicated to creating a single detail - Create a
Fixture
to set up the state of a singleInvoice
- Use the invoice detail
Fixture
as an embedded section within aFixture
for theInvoice
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();
}