SpecsFor MVC

I have been working with ASP.NET MVC project and as my site grew up, I began to miss integration test framework. I googled around to find one and eventually stumbled into SpecsFor MVC. SpecsFor.MVC (later SFM) is a BDD testing framework build on top of SpecsFor library. SFM uses Selenium to run tests and tests can be written by using nunit or mstests. Perfect!

Setup SFM

Setting up SFM on existing project is quite easy:
First create a new dll project
Add SpecsFor.MVC nuget package
Add NUnit nuget package (and NUnit test adapter if you want to run tests from VS)
Add reference to your MVC project
Create a new class called MvcAppConfig
Write following wiring code for SpecsFor:

[SetUpFixture]
public class MvcAppConfig
{
   private SpecsForIntegrationHost _host;

   [SetUp]
   public void SetupTestRun()
   {
      var config = new SpecsForMvcConfig();
      //SpecsFor.Mvc can spin up an instance of IIS Express to host your app
      
//while the specs are executing.     
      config.UseIISExpress()
      //To do that, it needs to know the name of the project to test...    
      .With(Project.Named("MyMVCProject.UnitSpecs"))
      //And optionally, it can apply Web.config transformations if you want it to.    
      .ApplyWebConfigTransformForConfig("Debug");

      //In order to leverage the strongly-typed helpers in SpecsFor.Mvc,
      //you need to tell it about your routes.  Here we are just calling
      //the infrastructure class from our MVC app that builds the RouteTable.    
      config.BuildRoutesUsing(r => RouteConfig.RegisterRoutes(r));
      //SpecsFor.Mvc can use either Internet Explorer or Firefox.  Support    
      //for Chrome is planned for a future release.    
      config.UseBrowser(BrowserDriver.InternetExplorer);

      //Does your application send E-mails?  Well, SpecsFor.Mvc can intercept    
      //those while your specifications are executing, enabling you to write    
      //tests against the contents of sent messages.    
      config.InterceptEmailMessagesOnPort(13565);

      //The host takes our configuration and performs all the magic.  We    
      //need to keep a reference to it so we can shut it down after all    
      //the specifications have executed.    
      _host = new SpecsForIntegrationHost(config);
   
      _host.Start();

   }
   //The TearDown method will be called once all the specs have executed.
   //All we need to do is stop the integration host, and it will take
   //care of shutting down the browser, IIS Express, etc.
   [TearDown]
   public void TearDownTestRun()
   {
      _host.Shutdown();
   }
}


And that's all. Best thing about SFM is that all tests are type safe. We rarely have to use magic strings to find elements from page and validation can be done by checking routes.

In my example I have a controller called AccountController which contains methods called Register. GET version of Register takes two parameter: ReturnUrl and useExtendedRegistration. Returnurl is used to set browser back to site where the registration was called and useExtendedRegistration is a feature to support two different kind of registrations. POST version takes RegisterViewModel as a parameter.
So in AccountController i have methods which are something like this:
public ActionResult Register(string returnUrl, bool? useExtendedRegistration) {
   … registration logic …
   return View();
}

And post version

[HttpPost]
public async Task<ActionResult> Register(RegisterViewModel model)
{
   ...
}

Then I have a RegisterViewModel which is property pack for registration:

public class
RegisterViewModel
{
   [Required]
   [EmailAddress]
   [Display]
   public string Email { get; set; }

   [Required]
   [Display]
   public string NickName { get; set; }

   [Display]
   public string Company { get; set; }

   …
}


And finally I have the view itself:

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
   @Html.Hidden("ReturnUrl", (string)ViewBag.ReturnUrl);
                        
   @Html.AntiForgeryToken()
   <h4>@Properties.Resources.Register_Text</h4>
   <hr />
   @Html.ValidationSummary("", new { @class = "text-danger" })
   <div class="form-group">
      @Html.LabelFor(m => m.Email, new { @class = "col-md-4 control-label" })
      <div class="col-md-8">
          @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
      </div>
    </div>
   <div class="form-group">
      @Html.LabelFor(m => m.NickName, new { @class = "col-md-4 control-label" })
      <div class="col-md-8">
         @Html.TextBoxFor(m => m.NickName, new { @class = "form-control" })
      </div>
    </div>

  …

Write integration test

To test this form I create a new test class called RegistrationSpecs. I mark class as TestFixture and add a variable called app into it:

[TestFixture]
public class RegistrationSpecs
{
   public MvcWebApp app;

   [TestFixtureSetUp]
   public void Setup()
   {
      app = new MvcWebApp();

   }
}

Next I write the actual test:

[Test]
public void Registering_With_Email_Success()
{
   // Navigate browser into register page
   app.NavigateTo<AccountController>(controller => controller.Register(null, false));
   // Use util method to create unique email address
   RegisteredEmail = Utils.CreateUniqueEmail();
   RegisteredPassword = "testpassword";
   // Find form from page

   app.FindFormFor<RegisterViewModel>()
      // Fill all the required fields
      .Field(f => f.Email).SetValueTo(RegisteredEmail)
      .Field(f => f.Password).SetValueTo(RegisteredPassword)
      .Field(f => f.ConfirmPassword).SetValueTo("testpassword")               
      .Field(f => f.NickName).SetValueTo("test" + DateTime.Now.Ticks)
      .Field(f => f.PhoneNumber).SetValueTo("123456789")
      .Field(f => f.FirstName).SetValueTo("FirstName")
      .Field(f => f.LastName).SetValueTo("LastName")
      .Field(f => f.Company).SetValueTo("testcompany" + DateTime.Now.Ticks)

      // Checkboxes are checked by calling click method
      .Field(f => f.RulesAccepted).Click()
      .Submit();

   // Validate that browser is taken into index page
   app.Route.ShouldMapTo<IndexController>(index => index.Index());
}


ShouldMapTo is an extension method from RouteTestingExtensions.

As seen in example, form finding and filling is done by using model class. This free us from magic strings and makes the test much more durable to changes (breaking change will reflect to test as compile errors). When I run the test I can see how selenium starts, opens browser, navigates into form page, fills form and submits its. If everything went well, my test is green in test explorer.

If you you want to do more complicated tests, you may have to call app.Browser.FindElement method to select correct elements, but thats only a good backup.

The best way to predict the future is to implement it.

1 kommentti: