ScenarioContext's Set-method

255 views
Skip to first unread message

b.b.

unread,
Jul 24, 2011, 9:34:51 AM7/24/11
to SpecFlow
This might not be an issue in the proper sense of the word. I wonder,
though, whether someone has come across the following issue yet. For
ScenarioContext's Set-method does accept T- and Func<T> arguments, a
simple typo can play you rather bad, like so:
ScenarioContext.Set(Foo.bar()) vs. ScenarioContext.Set(Foo.bar). Where
"Foo" would be a factory-like object mother and "bar()" its factory
method. Therefore, instead of preserving state between steps,
ScenarioContext.Get<T>() migth unexpectedly turn into a source of
rather obscure behaviour.

Darren Cauthon

unread,
Jul 24, 2011, 11:21:21 AM7/24/11
to SpecFlow

Hello b.b,

As the guy who added this feature to SpecFlow, let me explain
myself. :)

To set some context, I'm a big fan of organizing steps into step
classes that are related to one "thing," like a class or a concept.
So, for example, an Account domain object will have an AccountSteps
class, a UPS Integration Service will have a UpsSteps, etc.. Scenario
Context is great because it gives us a way to share values between the
steps related to different things.

I'm also a DIP fan, even when it comes to steps. I don't want steps
to be dependent on others. So if I ever need an IUpsService to setup
an instance of IMyNeatShippingHelperClass, I just want to pull it out
of ScenarioContext.Get<IUpsService>().

So here's the problem:

[Binding]
public class UpsServiceSteps(){
[BeforeScenario]
public void Setup(){
ScenarioContext.Current.Set<IUpsService>(new
Mock<IUpsService>().Object);
}
}

public class MyNeatShippingHelperClassSteps
[BeforeScenario]
public void Setup(){
var upsService = ScenarioContext.Current.Get<IUpsService>();
var myNeatShippingHelperClass = new
MyNeatShippingHelperClass(upsService);

ScenarioContext.Current.Set<IMyNeatShippingHelperClass>(myNeatShippingHelperClass);
}
}

This code may or may not work, depending on the order in which the
BeforeScenario methods are run. If the helper setup is run first, it
will throw because the UPS setup hasn't been run. These two step
files are now tied together.

So how do we fix this? Well, I think the simplest way is to do this:

ScenarioContext.Set<IMyNeatShippingHelperClass>(() => {
var upsService = ScenarioContext.Current.Get<IUpsService>();
return new MyNeatShippingHelperClass(upsService);
});

By delaying the creation to the point it is needed, we're guaranteed
to have everything set up.

Lately, I've hit this issue less frequently because I don't like "new"
calls in my specs. If I'm testing my entire app, how can I be sure
that how I've arranged my step definitions match the application?
However, I still think there are some situations where the factory
method can still be very useful.

There is a problem with the .Set<T>(Func<T>) method, but it's a
consistency issue. If you look at the code for Get<T> and Set<T>,
you'll see that it's just a type-safe wrapper around the
ScenarioContext dictionary. That means if you call .Set<T>(T obj),
you can get it out with a regular
ScenarioContext.Current.Get[typeof(T).FullName] (or something like
that) instead of .Get<T>(). But if you use Set<T> with a function,
you have to use Get<T> to execute the function. I doubt this behavior
will affect anyone, but I still feel bad about it.


Darren

b.b.

unread,
Jul 24, 2011, 4:59:54 PM7/24/11
to SpecFlow
Thanks a lot for the quick and detailed response. The point was the
typo. When I'am testing - pardon me - specifying, I'am in the modeling
mode, i.e. failing for the right reason as fast and as hard as
possible. ScenarioContext's Set<T>-method is just error-prone.
Renaming the Func<T>-overload would make it in fact less error-prone.
A simple case of cheap inspection to prevent vs. costly inspection to
find defects.
Reply all
Reply to author
Forward
0 new messages