Mar 29 2012

Using SpecsFor.Mvc - Navigation and Form Submission

Category: SpecsFor | TestingMatt @ 13:58

This is part two of my series on Using SpecsFor.Mvc to write awesome automated acceptance tests for your ASP.NET MVC application.  In this post, we’ll look at navigating around your app from SpecsFor.Mvc and at how to locate, populate, and submit forms. 

The “Using SpecsFor.Mvc” Series

Our Configuration

For these examples, we’ll again be using the SpecsFor.Demo application (available on Github) with a standard SpecsFor.Mvc configuration:

[SetUpFixture]
public class AssemblyStartup
{
    private SpecsForIntegrationHost _host;

    [SetUp]
    public void SetupTestRun()
    {
        var config = new SpecsForMvcConfig();
        config.UseIISExpress()
            .With(Project.Named("SpecsFor.Mvc.Demo"))
            .ApplyWebConfigTransformForConfig("Test");

        config.BuildRoutesUsing(r => MvcApplication.RegisterRoutes(r));
        config.UseBrowser(BrowserDriver.InternetExplorer);

        config.InterceptEmailMessagesOnPort(13565);

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

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

For those of you who didn’t read part one, this configuration tells SpecsFor.Mvc several things:

  • Use IIS Express to host the test instance of the site.
  • The site to test is in a project named “SpecsFor.Mvc.Demo.”
  • Apply the Web.Test.config transformation when deploying the site to IIS Express.
  • Build routes using the MvcApplication.RegisterRoutes method from our application.
  • Use Internet Explorer for testing.
  • Intercept all outgoing E-mail by monitoring port 13565 using a dummy SMTP server.

Finally, the configuration is passed to SpecsForIntegrationHost, and SpecsFor.Mvc takes care of wiring everything else up for you.

Navigation with SpecsFor.Mvc

SpecsFor.Mvc utilizes Selenium WebDriver under the hood, and you have full access to all the capabilities of WebDriver in your tests.  That means you can navigate to any URL using Selenium’s INavigation.GoToUrl method, like so:

public class when_a_new_user_registers : SpecsFor<MvcWebApp>
{
    protected override void Given()
    {
        SUT.Browser.Navigate().GoToUrl("http://localhost:4356/Account/Register");
    }
    //...snip...
}

The problem with this approach is that it’s built on magic strings.  If we were to rename our controller or action method, or if we were to change our routing, this URL may no longer be valid, and our test would fail for the wrong reason.

SpecsFor.Mvc provides a better way to perform navigation by using a strongly-typed API:

public class when_a_new_user_registers : SpecsFor<MvcWebApp>
{
    protected override void Given()
    {
        SUT.NavigateTo<AccountController>(c => c.Register());
    }
    //...snip...
}

Since NavigateTo uses strongly-typed lambda expressions, it is both refactor-friendly and unaffected by changes in routing.  The method fully supports navigation to action methods that accept parameters as well:

public class when_viewing_an_existing_invoice : SpecsFor<MvcWebApp>
{
    protected override void When()
    {
        SUT.NavigateTo<ViewInvoiceController>(c => c.Index("Customer", 1));
    }
}

Note: At this time, navigating to controllers within areas is not supported.  I’m actively investigating the issue and hope to have a resolution soon.  Pull requests are always appreciated, too. Smile 

Filling Out Forms

ASP.NET MVC provides numerous ways to create forms within your views.  You can go with raw HTML:

<form action="/Account/LogOn" method="post">
    <div>
        <fieldset>
            <legend>Account Information</legend>
            
            <--- SNIP -->
            
            <div class="editor-field">
                <input id="UserName" name="UserName" type="text" value="">
            </div>
            
            <--- SNIP -->
        </fieldset>
    </div>
</form>

or you can use string-based helper methods:

@using (Html.BeginForm()) {
    <div>
        <fieldset>
            <legend>Account Information</legend>

            <--- SNIP -->

            <div class="editor-field">
                @Html.TextBox("UserName")
            </div>

            <--- SNIP -->
            
        </fieldset>
    </div>
}

If you are using one of these methods, you could utilize the underlying Selenium API to access elements:

public class when_a_new_user_registers : SpecsFor<MvcWebApp>
{
    //...snip...

    protected override void When()
    {
        SUT.Browser.FindElement(By.Id("UserName")).SendKeys("Test User");
        SUT.Browser.FindElement(By.Id("Email")).SendKeys("test@user.com");
        SUT.Browser.FindElement(By.Id("Password")).SendKeys("P@ssword!");
        SUT.Browser.FindElement(By.Id("ConfirmPassword")).SendKeys("P@ssword!");
        SUT.Browser.FindElement(By.TagName("form")).Submit();
    }

    //...snip...
}

Again, this approach is loosely-typed and very likely to introduce maintenance problems both in your views as well as in your tests.  Tests should enable you to change your application with confidence, not introduce additional hurdles.  Fortunately there is a better way.

ASP.NET MVC provides a way to generate forms using strongly-typed lambda expressions over your view models:

@using (Html.BeginForm()) {
    <div>
        <fieldset>
            <legend>Account Information</legend>

            <--- SNIP -->

            <div class="editor-field">
                @Html.TextBoxFor(m => m.UserName)
            </div>

            <--- SNIP -->
            
        </fieldset>
    </div>
}

This approach is the most prevalent in the community and is the one that SpecsFor.Mvc works with out of the box. Assuming you have already directed SpecsFor.Mvc to a page containing a form, you can populate the form using strongly-typed lambda expressions which are again refactor-friendly:

public class when_a_new_user_registers : SpecsFor<MvcWebApp>
{
    //...snip...

    protected override void When()
    {
        SUT.FindFormFor<RegisterModel>()
            .Field(m => m.Email).SetValueTo("test@user.com")
            .Field(m => m.UserName).SetValueTo("Test User")
            .Field(m => m.Password).SetValueTo("P@ssword!")
            .Field(m => m.ConfirmPassword).SetValueTo("P@ssword!")
            .Submit();
    }

    //...snip...
}

Under the hood, SpecsFor.Mvc is utilizing the same conventions as ASP.NET MVC’s lambda-based helpers to determine the IDs for form elements based on your model.  Since both the view and the tests are based on the same conventions, your tests will stay in sync with any changes you make to your view. 

Note: At this time the conventions are sealed in, but they will be exposed so that they can be overridden with alternate conventions in a future release. 

After submitting a form, you can utilize MvcContrib.TestHelper to verify that your application redirected to the desired location:

public class when_a_new_user_registers : SpecsFor<MvcWebApp>
{
    //...snip...

    [Test]
    public void then_it_redirects_to_the_home_page()
    {
        SUT.Route.ShouldMapTo<HomeController>(c => c.Index());
    }

    //...snip...
}

The next version of SpecsFor.Mvc will include a configurable, convention-based method of verifying locations that’s also compatible with single-page applications.

Testing Validation

The final piece of interacting with forms via SpecsFor.Mvc is validation.  You can test both server-side and client-side validation.  Assuming you are using the out-of-the-box conventions, you can use the same fluent API for checking field validation as you use for filling out forms:

public class when_a_new_user_registers_with_invalid_data : SpecsFor<MvcWebApp>
{
    //...snip...

    protected override void When()
    {
        SUT.FindFormFor<RegisterModel>()
            .Field(m => m.Email).SetValueTo("notanemail")
            //.Field(m => m.UserName).SetValueTo("Test User") --Omit a required field.
            .Field(m => m.Password).SetValueTo("P@ssword!")
            .Field(m => m.ConfirmPassword).SetValueTo("SomethingElse")
            .Submit();
    }

    //...snip...

    [Test]
    public void then_it_marks_the_username_field_as_invalid()
    {
        SUT.FindFormFor<RegisterModel>()
            .Field(m => m.UserName).ShouldBeInvalid();
    }

    [Test]
    public void then_it_marks_the_email_as_invalid()
    {
        SUT.FindFormFor<RegisterModel>()
            .Field(m => m.Email).ShouldBeInvalid();
    }
}

You can also check the page’s Validation Summary:

public class when_logging_in_with_an_invalid_username_and_password : SpecsFor<MvcWebApp>
{
    protected override void Given()
    {
        SUT.NavigateTo<AccountController>(c => c.LogOn());
    }

    protected override void When()
    {
        SUT.FindFormFor<LogOnModel>()
            .Field(m => m.UserName).SetValueTo("bad@user.com")
            .Field(m => m.Password).SetValueTo("BadPass")
            .Submit();
    }

    //...snip...

    [Test]
    public void then_it_should_contain_a_validation_error()
    {
        SUT.ValidationSummary.Text.ShouldContain("The user name or password provided is incorrect.");
    }
}

The next version of SpecsFor.Mvc will also allow you to override the underlying conventions for locating validation messages (conventions are going to be a major theme in SpecsFor.Mvc 2.0).

What’s Next?

Submitting data is great, but what about reading it back out?  In the next post, I’ll show you how to find data on a page using SpecsFor.Mvc’s strongly-typed, refactor-friendly API.  In the meantime, check out the project on Github, or install the package via NuGet!

Tags:

blog comments powered by Disqus