Jan 1 2012

Status Update on SpecsFor.Mvc

Category: SpecsFor | TestingMatt @ 15:04

Well, I missed my goal of having the 1.0 version of SpecsFor.Mvc available by the end of 2011, but it was not for lack of trying.  I’ve been dog-fooding it on a mobile web app (my first), and that’s resulted in a number of changes and improvements.  I’m quite pleased with how things are shaping up, but I’d like some feedback from someone who isn’t me on the changes.

What’s the Point?

As I said, I’m definitely making progress on SpecsFor.Mvc.  While it’s come a very long way from the 0.1 release, I know it’s still not ready for a 1.0 release.  In fact, the latest code is currently sitting, forked, in the solution for my mobile app.  I’m frequently renaming and refactoring things to make them easier to use, and that seemed like the path of least-friction for now.  So, since you can’t run over to Github to check out the changes, this post will have to do.

Before I show off the code though, I suppose it would be helpful to outline what I’m thinking in terms of goals for the project.  Here are the goals/attributes/whatever I’m striving for with SpecsFor.Mvc:

  1. Integration tests for ASP.NET MVC applications that don’t become a pain point.
  2. Strongly-typed, refactor-friendly test code.
  3. Intuitive, fluent, and composable configuration API.
  4. In-the-box support for hosting the app-under-test with IIS Express.
  5. In-the-box support for establishing context (ie: seed data). 
  6. Pluggable conventions.

There are probably other attributes that I’m subconsciously striving for, but I think those are the major ones.  I’m meeting these with varying degrees of success as you’ll see in the examples.

The Configuration API

Since SpecsFor.Mvc deals with integration tests, there are certain cross-cutting concerns that need to be dealt with.  What’s the URL to the application?  What database should it use?  What data needs to be loaded into the database?  How are routes built in the application?  In the future, things like “how do I locate the form for this view model on a page?” will also be specifiable via the configuration API.  Let’s look at the configuration code for my mobile application.  First is the SetUpFixture:

[SetUpFixture]
public class AssemblyStartup
{
    private SpecsForIntegrationHost _integrationHost;

    [SetUp]
    public void SetupTestRun()
    {
        var config = new SpecsForMvcConfig();
        config.Use<CommonConfig>();
        config.Use<SeedDataConfig>();

        //TODO: Open questions to be answered:
        //1) How do we point the app at a database for testing?

        _integrationHost = new SpecsForIntegrationHost(config);
        _integrationHost.Start();
    }

    [TearDown]
    public void TearDownTestRun()
    {
        _integrationHost.Shutdown();
    }
}

Note: Setup fixtures are special NUnit fixtures that contain one-time setup and teardown logic for a namespace.  In this case, the fixture is in the root of my test project, so its setup is run prior to any of the actual test cases, and its tear down is run only after all other tests from the project have executed. 

During the fixture setup, I’m creating an instance of the SpecsForMvcConfig class.  This class is modeled after a StructureMap Registry.  It contains the ability to fully configure SpecsFor.Mvc’s options, or you can take a more composition approach and merge in separate configuration classes.  I’ve taken the later approach in this example.  Here are the other two config classes.  Note that the SeedDataConfig class is really just a placeholder.  I’m not sure how I want to handle seed data yet…

public class CommonConfig : SpecsForMvcConfig
{
    public CommonConfig()
    {
        UseIISExpressWith(Project.Named("HandyManager.Web"));
        BuildRoutesUsing(r => new RouteBootstrapper(r).Execute());
        UseBrowser(BrowserDriver.InternetExplorer);
    }
}

public class SeedDataConfig : SpecsForMvcConfig
{
    public SeedDataConfig()
    {
        BeforeEachTest(() =>
                           {
                               //TODO: Add some seed data.
                           });
    }
}

Once the config data is built up, it’s passed off to the SpecsForIntegrationHost class.  This class is actually trivially simple: it just executes the commands that have been built up by the configuration DSL.  In this case, that means spinning up an instance of IIS Express to host the actual application, registering routes, and telling SpecsFor.Mvc which browser to use.

The Test API

The test API has changed a bit since last time.  I’ve renamed a few things, added new helpers, etc.  Keep in mind that while the examples below were created using SpecsFor, SpecsFor.Mvc is actually a stand-alone library and can be used without the rest of SpecsFor (though why you’d want to use anything but SpecsFor, I’ll never know Smile ).

Here’s a test to make sure required fields on a form are marked as invalid when you try to submit an empty form:

public class when_adding_misc_entry_without_required_data : SpecsFor<MvcWebApp>
{
    protected override void Given()
    {
        SUT.NavigateTo<NewInvoiceController>(c => c.AddMiscCost());
    }

    protected override void When()
    {
        SUT.FindFormFor<AddMiscCostForm>()
            .Submit();
    }

    [Test]
    public void then_it_marks_the_description_as_invalid()
    {
        SUT.FindFormFor<AddMiscCostForm>()
            .Field(f => f.Description).ShouldBeInvalid();
    }

    [Test]
    public void then_it_marks_the_cost_as_invalid()
    {
        SUT.FindFormFor<AddMiscCostForm>()
            .Field(f => f.Cost).ShouldBeInvalid();
    }

    [Test]
    public void then_it_marks_the_markup_as_invalid()
    {
        SUT.FindFormFor<AddMiscCostForm>()
            .Field(f => f.Markup).ShouldBeInvalid();
    }
}

As was true in the 0.1 release, finding fields on a form is still strongly-typed and refactor-friendly.  Right now it assumes you’re using the built-in lambda-based HTML helpers (EditorFor, TextBoxFor, etc), but in the future I will make that convention easy to swap out with your own.

Here’s an example of actually filling out a form and submitting it, then verifying that a listview (a jQuery Mobile “control”) has the expected entry:

public class when_adding_misc_entry_with_required_data : SpecsFor<MvcWebApp>
{
    protected override void Given()
    {
        SUT.NavigateTo<NewInvoiceController>(c => c.AddMiscCost());
    }

    protected override void When()
    {
        SUT.FindFormFor<AddMiscCostForm>()
            .Field(f => f.Description).SetValueTo("Misc")
            .Field(f => f.Cost).SetValueTo("100")
            .Field(f => f.Markup).SetValueTo("10")
            .Submit();
    }

    [Test]
    public void then_it_redisplays_the_form()
    {
        SUT.Route.ShouldMapTo<NewInvoiceController>(c => c.AddMiscCost());
    }

    [Test]
    public void then_it_displays_the_misc_cost()
    {
        SUT.Mobile().Listview("existing-entries").ShouldContain(li => li.Text == "Misc");
    }
}

This example highlights an area that’s still rough: locating display data.  For whatever reason, I temporarily forgot that you can use strongly-typed helpers for display as well, so I’m going to correct that oversight soon.  That last bit for locating the listview will look more like this soon:

[Test]
public void then_it_displays_the_misc_cost()
{
    SUT.FindDisplayFor<AddMiscCostForm>()
        .Mobile().ListView(f => f.ExistingCosts).Items.ShouldContain(li => li.Text == "Misc");
}

What Do You Think?

It took a lot less code to show off all the work that’s gone into SpecsFor.Mvc over the past week than I thought it would.  I suppose that’s to be expected, a lot of the work has been thrown away, recreated, then thrown away again.  I feel like I’m heading in the right direction, but I’d love to get some feedback.  Please drop me a comment to let me know what you think about the APIs, what you think is missing, and what cool features I should consider adding.

Tags:

blog comments powered by Disqus