No support for inline delegates?

Dec 27, 2010 at 11:11 AM

I would have liked to do use inline delegates (maybe with string descriptions in a separate parameter?) or at least named delegates:

Action DoingSomeSetup = () => { bla bla bla;};
|Action CallingTheSUT = () => { other bla bla bla;};
Action ResultShouldBeExactlySoAndSo = () => { Assert(...); }

Story.WithScenario("Saving a feature")
        .Given(DoingSomeSetup)

        .When(CallingTheSUT)

        .Then(ResultShouldBeExactlySoAndSo)
.Execute();

Of course that doesn't work... Any ideas for workarounds?


 
Dec 28, 2010 at 6:02 PM

I also would like to be able to use inline lambda expressions in the Given/When/Then actions. This would save me from writing a bunch of simple one-liner helper methods that are never re-used. The problem with the example above is that there isn't a good way to decompile a lambda expression into readable text. Also, I think there's value in generating the step text from the executing code, rather than a static string.

I think expression trees might be the solution to this problem.

If you assign a lambda expression as an Expression<Action> (instead of just a plain old delegate) then the compiler treats it differently. It will compile the lambda expression into a traversable expression tree which includes meta data about the lambda expression which is otherwise not available.

I use these helper methods which leverage Expression trees for setting textbox values and clicking buttons. They have allowed me to write less code in my test classes and still rely on compile time checks, rather than reflection hacks which cold accomplish a similar outcome.

    public class ExpressionTreeParameterFormatAttribute : ParameterFormatAttribute
    {
        public override string Format(object value)
        {
            var expressionTree = (LambdaExpression) value;
            return ((MemberExpression) expressionTree.Body).Member.Name;
        }
    }

    [TestFixture, UnitTest]
    public class ExpressionTreeParameterFormatAttributeTestCase
    {
        [Test]
        public void ShouldDisplayVariableAndFieldName()
        {
            var sample = new Sample();

            new ExpressionTreeParameterFormatAttribute().Format((Expression<Func<string>>)(() => sample.Property))
                .Should().Equal("Property");
        }

        public class Sample
        {
            public string Property { get; set; }
        }
    }

    public class PresenterTestCaseBase
    {
        protected static void ISetThe_TextTo_<T>([ExpressionTreeParameterFormat]Expression<Func<T>> textBox, string value) where T : ITextControl
        {
            textBox.Compile()().Text = value;
        }

        protected static void IClick_([ExpressionTreeParameterFormat]Expression<Func<IButtonControl>> btn)
        {
            btn.Compile()().Raise(x => x.Click += (o, s) => { }, null, null);
        }

        protected static void The_TextIs_<T>([ExpressionTreeParameterFormat]Expression<Func<T>> textControl, string value) where T : ITextControl
        {
            textControl.Compile()().Text.Should().Equal(value);
        }
    }

I leverage these helpers like so:

		public void ShouldRecordDeviceOrder()
		{
			Story
				.Given(AvailableProducts, new[] { Pedometer, WeightScale })
				.And(IVisitTheOrderDevicePage)
				.And(ISelectAProduct, Pedometer)
				.And(ISetThe_TextTo_, () => View.AddressLine1, "1600 Pennsylvania Avenue Northwest")
				.And(ISetThe_TextTo_, () => View.AddressSuiteApartmentNumber, "")
				.And(ISetThe_TextTo_, () => View.AddressCity, "Washington D.C.")
				.And(ISetThe_TextTo_, () => View.AddressState, "DC")
				.And(ISetThe_TextTo_, () => View.AddressZipCode, "20500-0004")
				.And(ISetThe_TextTo_, () => View.AddressCountry, "United States")
                .When(IClick_, () => View.SubmitButton)
				.Then(ConfirmationMessageVisibleIs, true)
				.And(OrderPanelVisibleIs, false)
				.And(AnOrderIsRecorded)
				.And(TheOrderContains, new[] { Pedometer })
				.And(TheOrderWillBeShippedTo, new PersonAddress { Address = "1600 Pennsylvania Avenue Northwest", Address2 = "", City = "Washington D.C.", State = "DC", Zip = "20500-0004", Country="United States"})
				.Verify();
		}

This produces the following output:

Story is Health Coach product ordering
  In order to manage a participant's health
  As a Health Coach
  I want to order health products for a participant

      With scenario should record device order
        Given available products([Pedometer, Weight Scale])                                                     => Passed
          And I visit the order device page                                                                     => Passed
          And I select a product(Pedometer)                                                                     => Passed
          And I set the AddressLine1 text to 1600 Pennsylvania Avenue Northwest                                 => Passed
          And I set the AddressSuiteApartmentNumber text to ""                                                  => Passed
          And I set the AddressCity text to Washington D.C.                                                     => Passed
          And I set the AddressState text to DC                                                                 => Passed
          And I set the AddressZipCode text to 20500-0004                                                       => Passed
          And I set the AddressCountry text to United States                                                    => Passed
        When I click SubmitButton                                                                               => Passed
        Then confirmation message visible is(True)                                                              => Passed
          And order panel visible is(False)                                                                     => Passed
          And an order is recorded                                                                              => Passed
          And the order contains([Pedometer])                                                                   => Passed
          And the order will be shipped to(1600 Pennsylvania Avenue Northwest , Washington D.C., DC 20500-0004) => Passed

 

It should be possible to leverage Expression trees in the Given/When/Then actions to allow us to write less helper methods. The nice thing is that this sort of enhancement can be implemented as a set of extension methods outside of the StoryQ assembly.

-Caley

Coordinator
Dec 28, 2010 at 6:55 PM

Hi Addys and Caley.

Version 1 of StoryQ was actually heavily expression based (The Q in StoryQ was taken from LINQ, which introduced expressions to .NET). Although Addy's example would have to be Given(()=>DoingSomeSetup).

I feel like where we are now is a bit of an evolution - Once you've made a method for a step (although it might have seemed like an overhead when you first wrote it), then that method is immediately available for reuse in your next test.

I've tried quite hard to make it so that there's "one right way" to use StoryQ, so I'd be reluctant to add what you've asked for into the StoryQ trunk. However, I'd encourage you to take a fork, because it sounds like you need it and this would be quite an easy to add.

If you look at the Extensions class at the bottom of StoryQ.flit.g.cs (http://storyq.codeplex.com/SourceControl/changeset/view/32903802d3be#src%2fStoryQ%2fStoryQ.flit.g.cs), you'll see some similar extension methods already created. You can edit the .tt file so that it generates the methods you want here.

 

Let me know if that's not enough info to get started

Regards - Rob