Enable Automated Unit Testing
by Microsoft
This is step 12 of a free “NerdDinner” application tutorial that walks-through how to build a small, but complete, web application using ASP.NET MVC 1.
Step 12 shows how to develop a suite of automated unit tests that verify our NerdDinner functionality, and which will give us the confidence to make changes and improvements to the application in the future.
If you are using ASP.NET MVC 3, we recommend you follow the Getting Started With MVC 3 or MVC Music Store tutorials.
NerdDinner Step 12: Unit Testing
Let’s develop a suite of automated unit tests that verify our NerdDinner functionality, and which will give us the confidence to make changes and improvements to the application in the future.
Why Unit Test?
On the drive into work one morning you have a sudden flash of inspiration about an application you are working on. You realize there is a change you can implement that will make the application dramatically better. It might be a refactoring that cleans up the code, adds a new feature, or fixes a bug.
The question that confronts you when you arrive at your computer is – “how safe is it to make this improvement?” What if making the change has side effects or breaks something? The change might be simple and only take a few minutes to implement, but what if it takes hours to manually test out all of the application scenarios? What if you forget to cover a scenario and a broken application goes into production? Is making this improvement really worth all the effort?
Automated unit tests can provide a safety net that enables you to continually enhance your applications, and avoid being afraid of the code you are working on. Having automated tests that quickly verify functionality enables you to code with confidence – and empower you to make improvements you might otherwise not have felt comfortable doing. They also help create solutions that are more maintainable and have a longer lifetime - which leads to a much higher return on investment.
The ASP.NET MVC Framework makes it easy and natural to unit test application functionality. It also enables a Test Driven Development (TDD) workflow that enables test-first based development.
NerdDinner.Tests Project
When we created our NerdDinner application at the beginning of this tutorial, we were prompted with a dialog asking whether we wanted to create a unit test project to go along with the application project:
We kept the “Yes, create a unit test project” radio button selected – which resulted in a “NerdDinner.Tests” project being added to our solution:
The NerdDinner.Tests project references the NerdDinner application project assembly, and enables us to easily add automated tests to it that verify the application functionality.
Creating Unit Tests for our Dinner Model Class
Let’s add some tests to our NerdDinner.Tests project that verify the Dinner class we created when we built our model layer.
We’ll start by creating a new folder within our test project called “Models” where we’ll place our model-related tests. We’ll then right-click on the folder and choose the Add->New Test menu command. This will bring up the “Add New Test” dialog.
We’ll choose to create a “Unit Test” and name it “DinnerTest.cs”:
When we click the “ok” button Visual Studio will add (and open) a DinnerTest.cs file to the project:
The default Visual Studio unit test template has a bunch of boiler-plate code within it that I find a little messy. Let’s clean it up to just contain the code below:
[!code-csharpMain]
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using Microsoft.VisualStudio.TestTools.UnitTesting;
5: using NerdDinner.Models;
6:
7: namespace NerdDinner.Tests.Models {
8:
9: [TestClass]
10: public class DinnerTest {
11:
12: }
13: }
The [TestClass] attribute on the DinnerTest class above identifies it as a class that will contain tests, as well as optional test initialization and teardown code. We can define tests within it by adding public methods that have a [TestMethod] attribute on them.
Below are the first of two tests we’ll add that exercise our Dinner class. The first test verifies that our Dinner is invalid if a new Dinner is created without all properties being set correctly. The second test verifies that our Dinner is valid when a Dinner has all properties set with valid values:
[!code-csharpMain]
1: [TestClass]
2: public class DinnerTest {
3:
4: [TestMethod]
5: public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {
6:
7: //Arrange
8: Dinner dinner = new Dinner() {
9: Title = "Test title",
10: Country = "USA",
11: ContactPhone = "BOGUS"
12: };
13:
14: // Act
15: bool isValid = dinner.IsValid;
16:
17: //Assert
18: Assert.IsFalse(isValid);
19: }
20:
21: [TestMethod]
22: public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
23:
24: //Arrange
25: Dinner dinner = new Dinner {
26: Title = "Test title",
27: Description = "Some description",
28: EventDate = DateTime.Now,
29: HostedBy = "ScottGu",
30: Address = "One Microsoft Way",
31: Country = "USA",
32: ContactPhone = "425-703-8072",
33: Latitude = 93,
34: Longitude = -92,
35: };
36:
37: // Act
38: bool isValid = dinner.IsValid;
39:
40: //Assert
41: Assert.IsTrue(isValid);
42: }
43: }
You’ll notice above that our test names are very explicit (and somewhat verbose). We are doing this because we might end up creating hundreds or thousands of small tests, and we want to make it easy to quickly determine the intent and behavior of each of them (especially when we are looking through a list of failures in a test runner). The test names should be named after the functionality they are testing. Above we are using a “Noun_Should_Verb” naming pattern.
We are structuring the tests using the “AAA” testing pattern – which stands for “Arrange, Act, Assert”:
- Arrange: Setup the unit being tested
- Act: Exercise the unit under test and capture results
- Assert: Verify the behavior
When we write tests we want to avoid having the individual tests do too much. Instead each test should verify only a single concept (which will make it much easier to pinpoint the cause of failures). A good guideline is to try and only have a single assert statement for each test. If you have more than one assert statement in a test method, make sure they are all being used to test the same concept. When in doubt, make another test.
Running Tests
Visual Studio 2008 Professional (and higher editions) includes a built-in test runner that can be used to run Visual Studio Unit Test projects within the IDE. We can select the Test->Run->All Tests in Solution menu command (or type Ctrl R, A) to run all of our unit tests. Or alternatively we can position our cursor within a specific test class or test method and use the Test->Run->Tests in Current Context menu command (or type Ctrl R, T) to run a subset of the unit tests.
Let’s position our cursor within the DinnerTest class and type “Ctrl R, T” to run the two tests we just defined. When we do this a “Test Results” window will appear within Visual Studio and we’ll see the results of our test run listed within it:
Note: The VS test results window does not show the Class Name column by default. You can add this by right-clicking within the Test Results window and using the Add/Remove Columns menu command.
Our two tests took only a fraction of a second to run – and as you can see they both passed. We can now go on and augment them by creating additional tests that verify specific rule validations, as well as cover the two helper methods - IsUserHost() and IsUserRegisterd() – that we added to the Dinner class. Having all these tests in place for the Dinner class will make it much easier and safer to add new business rules and validations to it in the future. We can add our new rule logic to Dinner, and then within seconds verify that it hasn’t broken any of our previous logic functionality.
Notice how using a descriptive test name makes it easy to quickly understand what each test is verifying. I recommend using the Tools->Options menu command, opening the Test Tools->Test Execution configuration screen, and checking the “Double-clicking a failed or inconclusive unit test result displays the point of failure in the test” checkbox. This will allow you to double-click on a failure in the test results window and jump immediately to the assert failure.
Creating DinnersController Unit Tests
Let’s now create some unit tests that verify our DinnersController functionality. We’ll start by right-clicking on the “Controllers” folder within our Test project and then choose the Add->New Test menu command. We’ll create a “Unit Test” and name it “DinnersControllerTest.cs”.
We’ll create two test methods that verify the Details() action method on the DinnersController. The first will verify that a View is returned when an existing Dinner is requested. The second will verify that a “NotFound” view is returned when a non-existent Dinner is requested:
[!code-csharpMain]
1: [TestClass]
2: public class DinnersControllerTest {
3:
4: [TestMethod]
5: public void DetailsAction_Should_Return_View_For_ExistingDinner() {
6:
7: // Arrange
8: var controller = new DinnersController();
9:
10: // Act
11: var result = controller.Details(1) as ViewResult;
12:
13: // Assert
14: Assert.IsNotNull(result, "Expected View");
15: }
16:
17: [TestMethod]
18: public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {
19:
20: // Arrange
21: var controller = new DinnersController();
22:
23: // Act
24: var result = controller.Details(999) as ViewResult;
25:
26: // Assert
27: Assert.AreEqual("NotFound", result.ViewName);
28: }
29: }
The above code compiles clean. When we run the tests, though, they both fail:
If we look at the error messages, we’ll see that the reason the tests failed was because our DinnersRepository class was unable to connect to a database. Our NerdDinner application is using a connection-string to a local SQL Server Express file which lives under the _Data directory of the NerdDinner application project. Because our NerdDinner.Tests project compiles and runs in a different directory then the application project, the relative path location of our connection-string is incorrect.
We could fix this by copying the SQL Express database file to our test project, and then add an appropriate test connection-string to it in the App.config of our test project. This would get the above tests unblocked and running.
Unit testing code using a real database, though, brings with it a number of challenges. Specifically:
- It significantly slows down the execution time of unit tests. The longer it takes to run tests, the less likely you are to execute them frequently. Ideally you want your unit tests to be able to be run in seconds – and have it be something you do as naturally as compiling the project.
- It complicates the setup and cleanup logic within tests. You want each unit test to be isolated and independent of others (with no side effects or dependencies). When working against a real database you have to be mindful of state and reset it between tests.
Let’s look at a design pattern called “dependency injection” that can help us work around these issues and avoid the need to use a real database with our tests.
Dependency Injection
Right now DinnersController is tightly “coupled” to the DinnerRepository class. “Coupling” refers to a situation where a class explicitly relies on another class in order to work:
[!code-csharpMain]
1: public class DinnersController : Controller {
2:
3: DinnerRepository dinnerRepository = new DinnerRepository();
4:
5: //
6: // GET: /Dinners/Details/5
7:
8: public ActionResult Details(int id) {
9:
10: Dinner dinner = dinnerRepository.FindDinner(id);
11:
12: if (dinner == null)
13: return View("NotFound");
14:
15: return View(dinner);
16: }
Because the DinnerRepository class requires access to a database, the tightly coupled dependency the DinnersController class has on the DinnerRepository ends up requiring us to have a database in order for the DinnersController action methods to be tested.
We can get around this by employing a design pattern called “dependency injection” – which is an approach where dependencies (like repository classes that provide data access) are no longer implicitly created within classes that use them. Instead, dependencies can be explicitly passed to the class that uses them using constructor arguments. If the dependencies are defined using interfaces, we then have the flexibility to pass in “fake” dependency implementations for unit test scenarios. This enables us to create test-specific dependency implementations that do not actually require access to a database.
To see this in action, let’s implement dependency injection with our DinnersController.
Extracting an IDinnerRepository interface
Our first step will be to create a new IDinnerRepository interface that encapsulates the repository contract our controllers require to retrieve and update Dinners.
We can define this interface contract manually by right-clicking on the folder, and then choosing the Add->New Item menu command and creating a new interface named IDinnerRepository.cs.
Alternatively we can use the refactoring tools built-into Visual Studio Professional (and higher editions) to automatically extract and create an interface for us from our existing DinnerRepository class. To extract this interface using VS, simply position the cursor in the text editor on the DinnerRepository class, and then right-click and choose the Refactor->Extract Interface menu command:
This will launch the “Extract Interface” dialog and prompt us for the name of the interface to create. It will default to IDinnerRepository and automatically select all public methods on the existing DinnerRepository class to add to the interface:
When we click the “ok” button, Visual Studio will add a new IDinnerRepository interface to our application:
[!code-csharpMain]
1: public interface IDinnerRepository {
2:
3: IQueryable<Dinner> FindAllDinners();
4: IQueryable<Dinner> FindByLocation(float latitude, float longitude);
5: IQueryable<Dinner> FindUpcomingDinners();
6: Dinner GetDinner(int id);
7:
8: void Add(Dinner dinner);
9: void Delete(Dinner dinner);
10:
11: void Save();
12: }
And our existing DinnerRepository class will be updated so that it implements the interface:
[!code-csharpMain]
1: public class DinnerRepository : IDinnerRepository {
2: ...
3: }
Updating DinnersController to support constructor injection
We’ll now update the DinnersController class to use the new interface.
Currently DinnersController is hard-coded such that its “dinnerRepository” field is always a DinnerRepository class:
[!code-csharpMain]
1: public class DinnersController : Controller {
2:
3: DinnerRepository dinnerRepository = new DinnerRepository();
4:
5: ...
6: }
We’ll change it so that the “dinnerRepository” field is of type IDinnerRepository instead of DinnerRepository. We’ll then add two public DinnersController constructors. One of the constructors allows an IDinnerRepository to be passed as an argument. The other is a default constructor that uses our existing DinnerRepository implementation:
[!code-csharpMain]
1: public class DinnersController : Controller {
2:
3: IDinnerRepository dinnerRepository;
4:
5: public DinnersController()
6: : this(new DinnerRepository()) {
7: }
8:
9: public DinnersController(IDinnerRepository repository) {
10: dinnerRepository = repository;
11: }
12: ...
13: }
Because ASP.NET MVC by default creates controller classes using default constructors, our DinnersController at runtime will continue to use the DinnerRepository class to perform data access.
We can now update our unit tests, though, to pass in a “fake” dinner repository implementation using the parameter constructor. This “fake” dinner repository will not require access to a real database, and instead will use in-memory sample data.
Creating the FakeDinnerRepository class
Let’s create a FakeDinnerRepository class.
We’ll begin by creating a “Fakes” directory within our NerdDinner.Tests project and then add a new FakeDinnerRepository class to it (right-click on the folder and choose Add->New Class):
We’ll update the code so that the FakeDinnerRepository class implements the IDinnerRepository interface. We can then right-click on it and choose the “Implement interface IDinnerRepository” context menu command:
This will cause Visual Studio to automatically add all of the IDinnerRepository interface members to our FakeDinnerRepository class with default “stub out” implementations:
[!code-csharpMain]
1: public class FakeDinnerRepository : IDinnerRepository {
2:
3: public IQueryable<Dinner> FindAllDinners() {
4: throw new NotImplementedException();
5: }
6:
7: public IQueryable<Dinner> FindByLocation(float lat, float long){
8: throw new NotImplementedException();
9: }
10:
11: public IQueryable<Dinner> FindUpcomingDinners() {
12: throw new NotImplementedException();
13: }
14:
15: public Dinner GetDinner(int id) {
16: throw new NotImplementedException();
17: }
18:
19: public void Add(Dinner dinner) {
20: throw new NotImplementedException();
21: }
22:
23: public void Delete(Dinner dinner) {
24: throw new NotImplementedException();
25: }
26:
27: public void Save() {
28: throw new NotImplementedException();
29: }
30: }
We can then update the FakeDinnerRepository implementation to work off of an in-memory List<Dinner> collection passed to it as a constructor argument:
[!code-csharpMain]
1: public class FakeDinnerRepository : IDinnerRepository {
2:
3: private List<Dinner> dinnerList;
4:
5: public FakeDinnerRepository(List<Dinner> dinners) {
6: dinnerList = dinners;
7: }
8:
9: public IQueryable<Dinner> FindAllDinners() {
10: return dinnerList.AsQueryable();
11: }
12:
13: public IQueryable<Dinner> FindUpcomingDinners() {
14: return (from dinner in dinnerList
15: where dinner.EventDate > DateTime.Now
16: select dinner).AsQueryable();
17: }
18:
19: public IQueryable<Dinner> FindByLocation(float lat, float lon) {
20: return (from dinner in dinnerList
21: where dinner.Latitude == lat && dinner.Longitude == lon
22: select dinner).AsQueryable();
23: }
24:
25: public Dinner GetDinner(int id) {
26: return dinnerList.SingleOrDefault(d => d.DinnerID == id);
27: }
28:
29: public void Add(Dinner dinner) {
30: dinnerList.Add(dinner);
31: }
32:
33: public void Delete(Dinner dinner) {
34: dinnerList.Remove(dinner);
35: }
36:
37: public void Save() {
38: foreach (Dinner dinner in dinnerList) {
39: if (!dinner.IsValid)
40: throw new ApplicationException("Rule violations");
41: }
42: }
43: }
We now have a fake IDinnerRepository implementation that does not require a database, and can instead work off an in-memory list of Dinner objects.
Using the FakeDinnerRepository with Unit Tests
Let’s return to the DinnersController unit tests that failed earlier because the database wasn’t available. We can update the test methods to use a FakeDinnerRepository populated with sample in-memory Dinner data to the DinnersController using the code below:
[!code-csharpMain]
1: [TestClass]
2: public class DinnersControllerTest {
3:
4: List<Dinner> CreateTestDinners() {
5:
6: List<Dinner> dinners = new List<Dinner>();
7:
8: for (int i = 0; i < 101; i++) {
9:
10: Dinner sampleDinner = new Dinner() {
11: DinnerID = i,
12: Title = "Sample Dinner",
13: HostedBy = "SomeUser",
14: Address = "Some Address",
15: Country = "USA",
16: ContactPhone = "425-555-1212",
17: Description = "Some description",
18: EventDate = DateTime.Now.AddDays(i),
19: Latitude = 99,
20: Longitude = -99
21: };
22:
23: dinners.Add(sampleDinner);
24: }
25:
26: return dinners;
27: }
28:
29: DinnersController CreateDinnersController() {
30: var repository = new FakeDinnerRepository(CreateTestDinners());
31: return new DinnersController(repository);
32: }
33:
34: [TestMethod]
35: public void DetailsAction_Should_Return_View_For_Dinner() {
36:
37: // Arrange
38: var controller = CreateDinnersController();
39:
40: // Act
41: var result = controller.Details(1);
42:
43: // Assert
44: Assert.IsInstanceOfType(result, typeof(ViewResult));
45: }
46:
47: [TestMethod]
48: public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {
49:
50: // Arrange
51: var controller = CreateDinnersController();
52:
53: // Act
54: var result = controller.Details(999) as ViewResult;
55:
56: // Assert
57: Assert.AreEqual("NotFound", result.ViewName);
58: }
59: }
And now when we run these tests they both pass:
Best of all, they take only a fraction of a second to run, and do not require any complicated setup/cleanup logic. We can now unit test all of our DinnersController action method code (including listing, paging, details, create, update and delete) without ever needing to connect to a real database.
Side Topic: Dependency Injection Frameworks |
---|
Performing manual dependency injection (like we are above) works fine, but does become harder to maintain as the number of dependencies and components in an application increases. Several dependency injection frameworks exist for .NET that can help provide even more dependency management flexibility. These frameworks, also sometimes called “Inversion of Control” (IoC) containers, provide mechanisms that enable an additional level of configuration support for specifying and passing dependencies to objects at runtime (most often using constructor injection). Some of the more popular OSS Dependency Injection / IOC frameworks in .NET include: AutoFac, Ninject, Spring.NET, StructureMap, and Windsor. ASP.NET MVC exposes extensibility APIs that enable developers to participate in the resolution and instantiation of controllers, and which enables Dependency Injection / IoC frameworks to be cleanly integrated within this process. Using a DI/IOC framework would also enable us to remove the default constructor from our DinnersController – which would completely remove the coupling between it and the DinnerRepositorys. We won’t be using a dependency injection / IOC framework with our NerdDinner application. But it is something we could consider for the future if the NerdDinner code-base and capabilities grew. |
Creating Edit Action Unit Tests
Let’s now create some unit tests that verify the Edit functionality of the DinnersController. We’ll start by testing the HTTP-GET version of our Edit action:
[!code-csharpMain]
1: //
2: // GET: /Dinners/Edit/5
3:
4: [Authorize]
5: public ActionResult Edit(int id) {
6:
7: Dinner dinner = dinnerRepository.GetDinner(id);
8:
9: if (!dinner.IsHostedBy(User.Identity.Name))
10: return View("InvalidOwner");
11:
12: return View(new DinnerFormViewModel(dinner));
13: }
We’ll create a test that verifies that a View backed by a DinnerFormViewModel object is rendered back when a valid dinner is requested:
[!code-csharpMain]
1: [TestMethod]
2: public void EditAction_Should_Return_View_For_ValidDinner() {
3:
4: // Arrange
5: var controller = CreateDinnersController();
6:
7: // Act
8: var result = controller.Edit(1) as ViewResult;
9:
10: // Assert
11: Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
12: }
When we run the test, though, we’ll find that it fails because a null reference exception is thrown when the Edit method accesses the User.Identity.Name property to perform the Dinner.IsHostedBy() check.
The User object on the Controller base class encapsulates details about the logged-in user, and is populated by ASP.NET MVC when it creates the controller at runtime. Because we are testing the DinnersController outside of a web-server environment, the User object isn’t set (hence the null reference exception).
Mocking the User.Identity.Name property
Mocking frameworks make testing easier by enabling us to dynamically create fake versions of dependent objects that support our tests. For example, we can use a mocking framework in our Edit action test to dynamically create a User object that our DinnersController can use to lookup a simulated username. This will avoid a null reference from being thrown when we run our test.
There are many .NET mocking frameworks that can be used with ASP.NET MVC (you can see a list of them here: http://www.mockframeworks.com/). For testing our NerdDinner application we’ll use an open source mocking framework called “Moq”, which can be downloaded for free from http://www.mockframeworks.com/moq.
Once downloaded, we’ll add a reference in our NerdDinner.Tests project to the Moq.dll assembly:
We’ll then add a “CreateDinnersControllerAs(username)” helper method to our test class that takes a username as a parameter, and which then “mocks” the User.Identity.Name property on the DinnersController instance:
[!code-csharpMain]
1: DinnersController CreateDinnersControllerAs(string userName) {
2:
3: var mock = new Mock<ControllerContext>();
4: mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
5: mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);
6:
7: var controller = CreateDinnersController();
8: controller.ControllerContext = mock.Object;
9:
10: return controller;
11: }
Above we are using Moq to create a Mock object that fakes a ControllerContext object (which is what ASP.NET MVC passes to Controller classes to expose runtime objects like User, Request, Response, and Session). We are calling the “SetupGet” method on the Mock to indicate that the HttpContext.User.Identity.Name property on ControllerContext should return the username string we passed to the helper method.
We can mock any number of ControllerContext properties and methods. To illustrate this I’ve also added a SetupGet() call for the Request.IsAuthenticated property (which isn’t actually needed for the tests below – but which helps illustrate how you can mock Request properties). When we are done we assign an instance of the ControllerContext mock to the DinnersController our helper method returns.
We can now write unit tests that use this helper method to test Edit scenarios involving different users:
[!code-csharpMain]
1: [TestMethod]
2: public void EditAction_Should_Return_EditView_When_ValidOwner() {
3:
4: // Arrange
5: var controller = CreateDinnersControllerAs("SomeUser");
6:
7: // Act
8: var result = controller.Edit(1) as ViewResult;
9:
10: // Assert
11: Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
12: }
13:
14: [TestMethod]
15: public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {
16:
17: // Arrange
18: var controller = CreateDinnersControllerAs("NotOwnerUser");
19:
20: // Act
21: var result = controller.Edit(1) as ViewResult;
22:
23: // Assert
24: Assert.AreEqual(result.ViewName, "InvalidOwner");
25: }
And now when we run the tests they pass:
Testing UpdateModel() scenarios
We’ve created tests that cover the HTTP-GET version of the Edit action. Let’s now create some tests that verify the HTTP-POST version of the Edit action:
[!code-csharpMain]
1: //
2: // POST: /Dinners/Edit/5
3:
4: [AcceptVerbs(HttpVerbs.Post), Authorize]
5: public ActionResult Edit (int id, FormCollection collection) {
6:
7: Dinner dinner = dinnerRepository.GetDinner(id);
8:
9: if (!dinner.IsHostedBy(User.Identity.Name))
10: return View("InvalidOwner");
11:
12: try {
13: UpdateModel(dinner);
14:
15: dinnerRepository.Save();
16:
17: return RedirectToAction("Details", new { id=dinner.DinnerID });
18: }
19: catch {
20: ModelState.AddModelErrors(dinner.GetRuleViolations());
21:
22: return View(new DinnerFormViewModel(dinner));
23: }
24: }
The interesting new testing scenario for us to support with this action method is its usage of the UpdateModel() helper method on the Controller base class. We are using this helper method to bind form-post values to our Dinner object instance.
Below are two tests that demonstrates how we can supply form posted values for the UpdateModel() helper method to use. We’ll do this by creating and populating a FormCollection object, and then assign it to the “ValueProvider” property on the Controller.
The first test verifies that on a successful save the browser is redirected to the details action. The second test verifies that when invalid input is posted the action redisplays the edit view again with an error message.
[!code-csharpMain]
1: [TestMethod]
2: public void EditAction_Should_Redirect_When_Update_Successful() {
3:
4: // Arrange
5: var controller = CreateDinnersControllerAs("SomeUser");
6:
7: var formValues = new FormCollection() {
8: { "Title", "Another value" },
9: { "Description", "Another description" }
10: };
11:
12: controller.ValueProvider = formValues.ToValueProvider();
13:
14: // Act
15: var result = controller.Edit(1, formValues) as RedirectToRouteResult;
16:
17: // Assert
18: Assert.AreEqual("Details", result.RouteValues["Action"]);
19: }
20:
21: [TestMethod]
22: public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {
23:
24: // Arrange
25: var controller = CreateDinnersControllerAs("SomeUser");
26:
27: var formValues = new FormCollection() {
28: { "EventDate", "Bogus date value!!!"}
29: };
30:
31: controller.ValueProvider = formValues.ToValueProvider();
32:
33: // Act
34: var result = controller.Edit(1, formValues) as ViewResult;
35:
36: // Assert
37: Assert.IsNotNull(result, "Expected redisplay of view");
38: Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
39: }
Testing Wrap-Up
We’ve covered the core concepts involved in unit testing controller classes. We can use these techniques to easily create hundreds of simple tests that verify the behavior of our application.
Because our controller and model tests do not require a real database, they are extremely fast and easy to run. We’ll be able to execute hundreds of automated tests in seconds, and immediately get feedback as to whether a change we made broke something. This will help provide us the confidence to continually improve, refactor, and refine our application.
We covered testing as the last topic in this chapter – but not because testing is something you should do at the end of a development process! On the contrary, you should write automated tests as early as possible in your development process. Doing so enables you to get immediate feedback as you develop, helps you think thoughtfully about your application’s use case scenarios, and guides you to design your application with clean layering and coupling in mind.
A later chapter in the book will discuss Test Driven Development (TDD), and how to use it with ASP.NET MVC. TDD is an iterative coding practice where you first write the tests that your resulting code will satisfy. With TDD you begin each feature by creating a test that verifies the functionality you are about to implement. Writing the unit test first helps ensure that you clearly understand the feature and how it is supposed to work. Only after the test is written (and you have verified that it fails) do you then implement the actual functionality the test verifies. Because you’ve already spent time thinking about the use case of how the feature is supposed to work, you will have a better understanding of the requirements and how best to implement them. When you are done with the implementation you can re-run the test – and get immediate feedback as to whether the feature works correctly. We’ll cover TDD more in Chapter 10.
Next Step
Some final wrap up comments.
|