Creating Complex Objects Edit on GitHub


Also see Configuring Input Models with ModelFixture.

Over its lifetime (Storyteller has been in continous usage since 2009), Storyteller has been most valuable over classic xUnit testing tools when the specification domain is data intensive. As stated in the Automated Testing Best Practices, the Storyteller team strongly recommends that all the necessary system state should be set up in the specification itself. To that end, Storyteller exposes a pair of helper methods on the Fixture class to quickly create paragraph grammars to set up complex objects:

  1. Fixture.CreateObject<T>() -- the user is in charge of telling Storyteller how to initialize the object
  2. Fixture.CreateNewObject<T>() -- which is just syntactical sugar to call a default constructor on the type "T"

Both of these methods are helpers to create specialized paragraph grammars and can also be used to create table grammars as well.

Building Simple Objects with Setter Properties

If you need to quickly build very simple objects like data transfer objects (DTO's) that have a public, no-arg constructor and expose public properties, you can use the Fixture.CreateNewObject<T> method to build objects of "T".

For example, let's say that our problem domain includes the idea of an Address that is represented by a simple type like this below:


public class Address
{
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public string StateOrProvince { get; set; }
    public string Country { get; set; }
    public string PostalCode { get; set; }
}

The simplest possible way to put together a grammar that builds out a single Address object is this:


public IGrammar IfTheAddressIs()
{
    return CreateNewObject<Address>("If the new address is", x =>
    {
        x.SetAllPrimitivePropertiesSpecificToThisType();
    });
}

In usage, that results in html like this:

If you are logically asking okay, but where did my new Address object go?, it's now available as Fixture.CurrentObject. See Managing State during Execution for more information on that.

There are a couple other options to be more selective about the properties that get selected:

  • An overload of SetAllPrimitiveProperties(Func<PropertyInfo, bool>) that allows you to filter which properties should be set.
  • SetAllPrimitivePropertiesSpecificToThisType() -- only use properties that are defined on the Type of the object being created, so that you filter out any settable properties from base classes.
  • SetProperties() as shown below allows you to explicitly pick which properties to use in one method call:

[Hidden]
public IGrammar SelectAddressesExplicitly()
{
    return CreateNewObject<Address>("If the new address is", _ =>
    {
        _.SetProperties(o => o.Address1, o => o.City, o => o.StateOrProvince);
    });
}

Specifying and Customizing Properties

When and if you want more control over the Cell's in the object creation grammars, you can use this syntax instead to more explicitly select and configure the data collection:


public IGrammar IfTheAddressIs2()
{
    return CreateNewObject<Address>("If the new address is", _ =>
    {
        _.SetProperty(o => o.Address1);
        _.SetProperty(o => o.Address2);

        // Customize the cell for 'City'
        _.SetProperty(o => o.City)
            .Header("City or Township")
            .DefaultValue("Austin");

        // and more properties

    });
}

Using as the Basis for a Table

It is also possible to expose the CreateObject/CreateNewObject grammars in tabular form to quickly build a set of data using the AsTable(title) extension method.

In the case below, I am taking the grammar that was built in the previous section on customizing properties, and exposing that grammar as a table like this:


public IGrammar TheAddressesAre()
{
    return this["IfTheAddressIs2"]
        .AsTable("The addresses are");
}

Used in a specification, our original CreateNewObject paragraph turns into a table like this one:

Options for More Complicated Object Creation

All the examples above involved Storyteller calling a no argument, public constructor function for the type of object being created. While this has been effective for simple data structures like data transfer objects (DTO's), other objects will need to be created in some other way. That is okay, because Storyteller's Fixture.CreateObject() mechanism has some other mechanisms shown below to handle these cases.

The Storyteller team is considering some improvements to the mechanisms shown below and there may be some easier, cleaner alternatives in the future. Your feedback or suggestions would be welcome.

Using a Sentence Grammar to Initialize

You can use another sentence grammar with the LoadObjectBy = [grammar] syntax to do the initial creation of the object within the CreateObject grammar as long as it will set the newly created object on Fixture.CurrentObject.

Below is a sample:


[Hidden]
[FormatAs("Street Address {line1}, {line2}")]
public void AddressIs(string line1, [DefaultValue("EMPTY")]string line2)
{
    CurrentObject = new Address
    {
        Address1 = line1,
        Address2 = line2
    };
}

public IGrammar AddressByStreet()
{
    return CreateObject<Address>("The address by street is", _ =>
    {
        _.LoadObjectBy = this["AddressIs"];

        _.SetProperty(o => o.City);
        _.SetProperty(o => o.StateOrProvince);
        // and the rest of the properties
    });
}

In usage, the grammar above looks like this:

Initialize the Object Yourself

Another option is to just silently initialize the object yourself with the ObjectIs = Func<ISpecContext, T> syntax shown below:


public IGrammar AddressWithObjectIs()
{
    return CreateObject<Address>("An address in Dallas, TX", _ =>
    {
        _.ObjectIs = c => new Address
        {
            City = "Dallas", 
            StateOrProvince = "TX", 
            Country = "USA"
        };

        _.SetProperty(o => o.Address1);
        _.SetProperty(o => o.Address2);
    });
}

The ObjectIs step of the grammar is executed, but not part of the rendered html for the grammar. A specification using the grammar in the sample above looks like this below:

Altering the Object without Using Properties

By no means will using settable properties be the only way you will ever need for configuring objects. As a "catch all" solution for everything else, Storyteller exposes the WithInput syntax shown below to collect a single cell of information and use that to alter the subject of the CreateObject grammar with a user-supplied Action. That syntax is demonstrated below:


public IGrammar AddressWithStreetAddress()
{
    return CreateNewObject<Address>("The address by street is", _ =>
    {
        _.WithInput<string>("The street address is {street}").Configure((address, data) =>
        {
            var parts = data.Split(',');
            address.Address1 = parts.First();
            address.Address2 = parts.Length == 2
                ? parts[1]
                : null;
        });

        _.SetProperty(o => o.City);
        _.SetProperty(o => o.StateOrProvince);
        // and the rest of the properties
    });
}

In the sample above, the input for "street" is a string, but you can also use the generic arguments to the WithInput<T>() to use any type that Storyteller can convert.

In usage, the grammar we built above renders as this:

"Silent" Actions with the Subject

Sooner or later you need to actually do something with the object you just created. The Do(Action<T>) method allows you to add a "silent" grammar to carry out user-supplied actions on the newly built subject.

The following code is an example:


public IGrammar IfTheAddressIs3()
{
    return CreateNewObject<Address>("If the new address is", x =>
    {
        x.SetAllPrimitivePropertiesSpecificToThisType();

        x.Do(address =>
        {
            // Add the new address to a list somewhere?
            // Set a private field on the Fixture?
            // Stash in Context.State?
            // WriteToText to a database or file?
            // Send to another service as an input?
        });
    });
}