Specifications are largely created by entering text into the editor and stored on disk as Xml (I know, but backward compatibility mattered). Storyteller 3 features a new conversion function to help convert the raw string data into the correct Types your fixture and grammar code needs to execute.
Even though the subsystem and this topic both use the "Conversion" nomenclature, the resolution from a raw string to the real value in actual usage has been:
- Doing a straight conversion from a string to a scalar value like a number
- Parsing text to create a more complex object to make specifications more terse
- Using the raw text to lookup data in the system under test using the system's persistence infrastructure
- Creating missing system data on the fly if it did not already exist
The conversion subsystem supports the most common Cell types out of the box, but also provides a couple of extension points that should accomodate any need.
What's in the Box?
Out of the box, the conversions can handle converting raw string input to .Net types for:
- Strings -- with the small caveat that "NULL" and "EMPTY" have special meaning as a null value and an empty, zero length string
- Every number type in .Net using the default
int.Parse()
method in .Net - Booleans -- uses a case insensitive match on "true" or "false"
- Enumerations -- converts based on the string option name for the value
- Nullable<T> -- assuming that Storyteller knows how to convert a string to whatever "T" is. "NULL" is converted to a null value.
- Array of T -- again, assuming that Storyteller understands how to convert a string to the type "T", this conversion works by treating the raw text as a comma delimited string of values.
- ctor(string) -- Any concrete type that has a public constructor function with a single parameter that takes in a string can be converted by calling that constructor function with the raw value. Or "NULL" to denote a null value.
- DateTimes -- more on this below.
By ctor(string)
A very powerful mechanism in Storyteller's conversion subsystem is the ability to just call a public constructor of a Type that accepts a single string argument.
From the unit tests in the Storyteller code, we have a type called Color
that exposes a public constructor matching this criteria:
public class Color
{
public string Name { get; set; }
public Color(string name)
{
this.Name = name;
}
}
In action, the conversion code selects the "call ctor(string)" strategy and this test below passes:
[Fact]
public void string_ctor_conversion()
{
new Conversions().Convert(typeof(Color), "Red")
.ShouldBeOfType<Color>()
.Name.ShouldBe("Red");
}
The inspiration for this feature was taken from a software system that tracked steel members. We frequently used a value type called Dimension
that was frequently described in short hand like: 1 x 1 x 36
that expressed the physical cross section and length of a steel piece. That type looked something like this:
public class Dimensions
{
public Dimensions()
{
}
// This constructor would be called by Storyteller
public Dimensions(string text)
{
var parts = text.ToDelimitedArray('x');
Width = int.Parse(parts[0]);
Thickness = int.Parse(parts[1]);
Length = int.Parse(parts[2]);
}
public int Width { get; set; }
public int Length { get; set; }
public int Thickness { get; set; }
}
Using this conversion may allow you to express hierarchical data in a concise, more readable format than you would be able to achieve by strictly supplying raw values for every raw property of a type.
You can think of this pattern as a built in interpreter pattern -- crude as it may be.
Custom Conversions
Custom conversions come in two different flavors, with the simpler version being the IConversionProvider
shown below:
public interface IConversionProvider
{
// Given the type argument, either return a
// Func that can parse a string into that Type
// or return null to let another IConversionProvider
// handle this type
Func<string, object> ConverterFor(Type type);
}
Internally, Storyteller treats all the conversion providers as a chain of responsibility such that the first provider that "knows" how to handle a type provides the actual conversion strategy as a simple Func<string, object>
func. User supplied providers are evaluated before the built in providers.
A sample provider is the EnumerationConversion
taken from the Storyteller code itself that handles any .Net Enumeration type:
public class EnumerationConversion : IConversionProvider
{
public Func<string, object> ConverterFor(Type type)
{
if (type.GetTypeInfo().IsEnum)
{
return x => Enum.Parse(type, x);
}
return null;
}
}
Runtime Conversions
Internally, Storyteller can apply the non-runtime conversions as a kind of "warmup" task to get a specification ready to execute before passing control to the actual specification engine. This was done as a performance optimization in the 3.0 release because of throughput issues in a very large project using an older version. Runtime conversions have to be delayed until the specification is executing, so there is a small performance penalty.
The more complex extension point is the IRuntimeConverter
that is allowed to use anything exposed by the ISpecContext
, including system services, to do the conversion/location/resolution from raw text to the requested type.
The signature is shown below:
public interface IRuntimeConverter
{
object Convert(string raw, ISpecContext context);
bool Matches(Type type);
}
A sample usage from the internal unit tests is shown below:
public class PlayerConverter : IRuntimeConverter
{
public static readonly IList<Player> Players = new List<Player>
{
new Player {FirstName = "Justin", LastName = "Houston", Position = "LB"},
new Player {FirstName = "Jeremy", LastName = "Maclin", Position = "WR"},
new Player {FirstName = "Jamaal", LastName = "Charles", Position = "RB"}
};
public object Convert(string raw, ISpecContext context)
{
return Players.FirstOrDefault(x => x.FullName() == raw);
}
public bool Matches(Type type)
{
return typeof (Player) == type;
}
}
public class Player
{
public string FirstName;
public string LastName;
public string Position;
public string FullName()
{
return "{0} {1}".ToFormat(FirstName, LastName);
}
}
Registering Custom Converters
Part of the signature of a custom ISystem
is to return a CellHandling
object that holds everything Storyteller needs to know in order to, well, handle all the cells in the specification system -- including any custom conversions.
The Conversions
property of CellHandling
allows you to add custom IConversionProvider
and IRuntimeConverter
types and objects as in this sample below:
public CellHandling Start()
{
var handling = CellHandling.Basic();
// Adding a system wide list.
handling.AddSystemLevelList("positions", new[] {"LB", "OL", "DL", "WR", "RB"});
// This is where you can register a custom runtime conversion
handling.RegisterRuntimeConversion<PlayerConverter>();
return handling;
}
DateTimes
The "TODAY +/- #" nomenclature was invented to deal with common business logic that dealt with relative dates. The original Storyteller team felt that using this nomenclature made the specifications easier to understand in terms of the desired logic by using relative dates than it was to use hard coded dates. Plus it helps you set up test data for relative date logic like "send an email if the open issue is more than 45 days old" that can be expressed in test setup as "IssueDate = TODAY - 46."
Date and time logic is so common that Storyteller has some special handling for expressing dates. The following rules in order of precedence are available:
- "[Day of the Week] [24 hour time]." -- So that "Wednesday 14:30" translates as 2:30 PM on the first Wednesday in the future. If today's day matches the day of the week, it select today's date.
- "TODAY" -- equivalent to DateTime.Today()
- "TODAY+#" or "TODAY-#" -- equivalent to DateTime.Today().Add(#.Days()).
- ISO 8601 formatted dates
- The default .Net DateTime.Parse(text) method