It’s been a long time coming, but I finally shipped version 1.0 of SpecsFor.Mvc last week. There’s a slew of features in this release. Enough, in fact, for a series of blog posts. Hence this post! This is the first of many covering what you can do with SpecsFor.Mvc 1.0. Read on, and I’ll show you everything that’s in the box!
[more]
The “Using SpecsFor.Mvc” Series
- Using SpecsFor.Mvc – Introduction (you are here)
- Using SpecsFor.Mvc – Navigation and Form Submission
- Using SpecsFor.Mvc – Reading Data
- Using SpecsFor.Mvc – Dealing with Authentication
- Using SpecsFor.Mvc – Establishing Context with Seed Data
- More coming soon!
TL;DR Version
SpecsFor.Mvc is out today. Install the NuGet package (and install SpecsFor as well if you really want to boost your productivity), then get started writing fully-automated acceptance tests!
Install-Package SpecsFor #SpecsFor.Mvc doesn't actually require SpecsFor, but it certainly plays nicely with it! Install-Package SpecsFor.Mvc
Check it out!
public class when_a_new_user_registers : SpecsFor<MvcWebApp> { protected override void Given() { SUT.NavigateTo<AccountController>(c => c.Register()); } protected override void When() { SUT.FindFormFor<RegisterModel>() .Field(m => m.Email).SetValueTo("[email protected]") .Field(m => m.UserName).SetValueTo("Test User") .Field(m => m.Password).SetValueTo("[email protected]!") .Field(m => m.ConfirmPassword).SetValueTo("[email protected]!") .Submit(); } [Test] public void then_it_redirects_to_the_home_page() { SUT.Route.ShouldMapTo<HomeController>(c => c.Index()); } [Test] public void then_it_sends_the_user_an_email() { SUT.Mailbox().MailMessages.Count().ShouldEqual(1); } [Test] public void then_it_sends_to_the_right_address() { SUT.Mailbox().MailMessages[0].To[0].Address.ShouldEqual("[email protected]"); } [Test] public void then_it_comes_from_the_expected_address() { SUT.Mailbox().MailMessages[0].From.Address.ShouldEqual("[email protected]"); } }
Motivation and History
This is a project that’s been brewing in the back of my mind since the ASP.NET MVC 1.0 days, when Jimmy Bogard presented his approach to integration testing to the C4MVC user group. Version 1.0 of SpecsFor.Mvc was driven from actual use on a jQuery Mobile application I wrote (more on that in a future post perhaps), and its feature set grew to meet my testing needs. This means that version 1.0 shipped with much more than I had originally planned. You can test everything from client-side script to E-mail sending with SpecsFor.Mvc, all with a strongly-typed, unit-test-like syntax. It can apply Web.config transforms prior to starting tests, there are hooks you can use to load test data into the app, and you can globally handle authentication without having to specify credentials in each spec. There’s a lot to cover, but first, let’s look at some of the challenges you would typically face in creating automated acceptance tests.
Challenges of Automated Acceptance Tests
One of the great benefits of unit testing is that it gives you the confidence to change your code. The idea is that if a change breaks something, a test will fail, and you’ll be able to correct the code long before it creeps into production. Good tests don’t get in the way of making changes to your code, they empower you to make changes by giving you confidence that everything still works as intended.
Unfortunately, one of the big challenges with any sort of test involving UI automation is creating tests that don’t become a barrier to making changes. Take a look at this example using Watin taken from a CodePlex article:
[Test] public void CheckIfNicknameIsNotUsed() { // create a new Internet Explorer instance // pointing to the ASP.NET Development Server using (IE ie = new IE("http://localhost:8080/Default.aspx")) { // Maximize the IE window in order to view test ie.ShowWindow(NativeMethods.WindowShowStyle.Maximize); // search for txtNickName and type "gsus" in it ie.TextField(Find.ById("txtNickName")).TypeText("gsus"); // fire the click event of the button ie.Button(Find.ById("btnCheck")).Click(); // parse the response in order to fail the test or not Assert.AreEqual(true, ie.ContainsText("The nickname is not used")); } }
Notice the liberal use of magic strings in this test. What happens if you refactor the underlying code? You’ll have to manually update this test because it is in no way strongly-tied to the application. That means this test is not empowering you to make changes, it’s actually introducing resistance to making changes. Friction, also referred to as rigid code, in an application is one of the signs of Bad Code.
Another challenge to crafting automated acceptance tests is the learning curve. Watin, Selenium, etc. all require you to learn their low-level API, and you have to worry about concerns that you typically don’t have to think about when performing unit tests, such as disposing of the browser, loading up seed data, and indeed even where to point the browser at for testing purposes.
SpecsFor.Mvc addresses all of those challenges, all by just installing a simple NuGet package.
Installing and Getting Started
SpecsFor.Mvc is an independent library for automated acceptance tests, but it plays so nicely with the core SpecsFor framework that you would be crazy not to use the two together, and that’s exactly what we’ll be doing in this series of posts. Let’s assume you already have an ASP.NET MVC application that you want to create integrated acceptance tests for. To get started with SpecsFor.Mvc, create a class library to serve as your test project, then install the SpecsFor and SpecsFor.Mvc NuGet packages:
Install-Package SpecsFor #SpecsFor.Mvc doesn't actually require SpecsFor, but it certainly plays nicely with it! Install-Package SpecsFor.Mvc
To simplify things further, you may want to install the Resharper or Visual Studio code snippets for SpecsFor, which are included in the SpecsFor package. They provide snippets and templates for creating test fixtures, creating specs, etc.
SpecsFor.Mvc includes a test helper object called MvcWebApplication. This is the class you will create your specs around, like so:
public class when_testing_a_feature_in_your_app : SpecsFor<MvcWebApp> { protected override void When() { //TODO: Interact with your app! } //TODO: Write specs to verify behavior! }
Before we can start writing specs though, we need to tell SpecsFor.Mvc a little bit about the project its going to test.
Configuring SpecsFor.Mvc
SpecsFor.Mvc uses a fluent DSL (Domain-Specific Language) for configuration. You can build up multiple SpecsForMvcConfiguration objects and compose them together, or you can build up a single configuration instance. This configuration must be built up and passed to SpecsForIntegrationHost prior to any of your specifications executing. If you are using NUnit, you can utilize a SetUpFixture to perform this configuration. If you are using MS-Test, you can use an AssemblyInitialize method. Let’s look at an annotated example using NUnit:
//An NUnit SetUpFixture is executed before //any tests in the fixture's namespace or //subnamespaces. Usually you would put //this in the root of your test project. [SetUpFixture] public class AssemblyStartup { 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("SpecsFor.Mvc.Demo")) //And optionally, it can apply Web.config transformations if you want //it to. .ApplyWebConfigTransformForConfig("Test"); //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 => MvcApplication.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(); } }
There’s not much config here, but we’re enabling SpecsFor.Mvc to do a lot. We’re telling it which project we are creating tests for, which Web.config transformation to apply, what the routes are for our application, which browser to use, and that we want it to intercept E-mail messages. Once we start the integration host, it will take care of doing all the magic for us, we just need to write our specs.
A Simple Test Case
Now that SpecsFor.Mvc is configured, we can create a test. Let’s look at a simple example of navigating to a form, filling in data, submitting it, and verifying the results. First, let’s navigate to the form:
public class when_logging_in_with_valid_credentials : SpecsFor<MvcWebApp> { protected override void Given() { SUT.NavigateTo<AccountController>(c => c.LogOn()); } //...snip... }
Notice that we’re using strongly-typed lambda expressions to navigate. This makes our test refactor-friendly. We can rename the action method using Resharper (or Visual Studio’s refactoring tools if you just hate yourself), and our test will be updated as well.
Next, let’s fill out the form on the page. Assuming we’re generating strongly-typed views (AND YOU SHOULD BE!), you can again use strongly-typed lambda expressions to fill out the form:
public class when_logging_in_with_valid_credentials : SpecsFor<MvcWebApp> { //...snip... protected override void When() { SUT.FindFormFor<LogOnModel>() .Field(m => m.UserName).SetValueTo("[email protected]") .Field(m => m.Password).SetValueTo("RealPassword") .Submit(); } //...snip... }
Once again, our test remains refactor-friendly. If we rename a property on our model, the test will be updated as well.
Finally, let’s verify the outcome. In this case, submitting the form should log the user in and take them to the home page:
public class when_logging_in_with_valid_credentials : SpecsFor<MvcWebApp> { //...snip... [Test] public void then_it_redirects_to_the_home_page() { SUT.Route.ShouldMapTo<HomeController>(c => c.Index()); } }
This verifies what should happen if valid input is supplied, but what if we were to fill out the form with invalid values?
Testing Validation
SpecsFor.Mvc can easily test client-side validation. Let’s create a new spec that submits a form with some invalid data:
public class when_a_new_user_registers_with_invalid_data : SpecsFor<MvcWebApp> { protected override void Given() { SUT.NavigateTo<AccountController>(c => c.Register()); } 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("[email protected]!") .Field(m => m.ConfirmPassword).SetValueTo("SomethingElse") .Submit(); } //...snip... }
We can then verify that we didn’t navigate to a new page, and that the form fields were marked as invalid, all in a strongly-typed manner:
public class when_a_new_user_registers_with_invalid_data : SpecsFor<MvcWebApp> { //...snip... [Test] public void then_it_redisplays_the_form() { SUT.Route.ShouldMapTo<AccountController>(c => c.Register()); } [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(); } }
There’s still more though. What about applications that send E-mail messages? SpecsFor.Mvc can help you test those behaviors as well.
Checking E-mail
SpecsFor.Mvc includes a light-weight SMTP server that can intercept E-mail messages. In order to use this feature, you have to also take advantage of another SpecsFor.Mvc feature: Web.config transformations. Create a ‘Test’ build configuration for your MVC project, and create a Web.Test.config tranform that will change your SMTP server to point to a known port on the local machine. Here’s an example:
<?xml version="1.0"?> <!-- For more information on using web.config transformation visit http://go.microsoft.com/fwlink/?LinkId=125889 --> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.net> <mailSettings> <smtp> <network host="localhost" port="13565" xdt:Transform="SetAttributes"/> </smtp> </mailSettings> </system.net> </configuration>
Since we already told SpecsFor.Mvc to intercept E-mail on this port (see the configuration above), we can now write specifications that verify sent E-mails:
public class when_a_new_user_registers : SpecsFor<MvcWebApp> { protected override void Given() { SUT.NavigateTo<AccountController>(c => c.Register()); } protected override void When() { SUT.FindFormFor<RegisterModel>() .Field(m => m.Email).SetValueTo("[email protected]") .Field(m => m.UserName).SetValueTo("Test User") .Field(m => m.Password).SetValueTo("[email protected]!") .Field(m => m.ConfirmPassword).SetValueTo("[email protected]!") .Submit(); } [Test] public void then_it_redirects_to_the_home_page() { SUT.Route.ShouldMapTo<HomeController>(c => c.Index()); } [Test] public void then_it_sends_the_user_an_email() { SUT.Mailbox().MailMessages.Count().ShouldEqual(1); } [Test] public void then_it_sends_to_the_right_address() { SUT.Mailbox().MailMessages[0].To[0].Address.ShouldEqual("[email protected]"); } [Test] public void then_it_comes_from_the_expected_address() { SUT.Mailbox().MailMessages[0].From.Address.ShouldEqual("[email protected]"); } }
SpecsFor.Mvc exposes the actual E-mail message that was received by the embedded SMTP server, so you can verify the recipients, subject, body, attachments, etc.
Just Scratching the Surface
This post has only touched on some of the things that SpecsFor.Mvc can do for you, and only at a very high level. In the next post in this series, we’ll start diving deep into the various features and how you can leverage them to create automated acceptance tests that empower you and your team to make changes and drive the development of new features. In the meantime, check out the code on Github, install the NuGet packages, and let me know what you think!