As anyone that knows me knows, I use test driven development. So when I put together a Feedback page in my MVC (Model-View-Controller) site I started with my test. What should the test be?
Let’s start with the User Story : As a site user (anonymous or logged in) I want to submit feedback to the webmaster so that I can get bugs fixed or get features I want.
So the HelpController has a view called Feedback that lets a user input some feedback and submit it using the CodeSmith Insight product.
I finally settled on a test like this…
- [TestCategory("Build"), TestCategory("Unit"), TestMethod]
- public void FeedbackReceived()
- {
- // Arrange
- string expected = "FeedbackReceived";
- string actual;
- var detail = new CaseReportDetail() { Description = "Test", Title = "Test" };
- var controller = CreateControllerAsNonAdmin(false);
- // Act
- var result = controller.Feedback(detail) as ViewResult;
- // Assert
- Assert.IsTrue(controller.IsCaseSubmited); // Was the case submitted?
- actual = result.ViewName;
- Assert.AreEqual(expected, actual); // Did it forward to the right view?
- }
That seems simple enough, but there’s obviously some hidden details here… What’s a CaseReportDetail? What’s the controller? And what’s the property IsCaseSubmitted?
CaseReportDetail
The CaseReportDetail is merely a model that I leveraged from the samples that came with the CodeSmith Insight. Here is my class:
- public class CaseReportDetail
- {
- public string Email { get; set; }
- public string Title { get; set; }
- public string Description { get; set; }
- }
Ok… that’s pretty simple, now what about the controller?
The Controller
The controller is a bit trickier. That code leverages the Moq (pronounced Mock You) framework. Here is what that code looks like:
- private HelpController CreateControllerAsNonAdmin(bool submitCaseFails)
- {
- var mock = new Mock<ControllerContext>();
- mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns("test@nowhere.com");
- mock.Setup(p => p.HttpContext.User.IsInRole("Admin")).Returns(false);
- mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);
- var controller = new HelpController(new MockInsightManager(submitCaseFails));
- controller.ControllerContext = mock.Object;
- return controller;
- }
But as we drill into that it begs a question, “What is the MockInsightManager?” As with many classes, the InsightManager does not have an interface and has some “non-overrideable” members that make Mocking hard. One trick I use when I encounter that is a wrapper with an interface. So let me show all these items, the IInsightManagerBase, InsightManagerWrapper, and MockInsightManager so you can get a sense for the overall layout:
- public interface IInsightManagerBase
- {
- bool IsCaseSubmitted { get; }
- CaseReport CreateCase();
- void SubmitCase(CaseReport caseReport);
- }
- public class InsightManagerWrapper : IInsightManagerBase
- {
- public InsightManagerWrapper()
- {
- Manager = InsightManager.Current;
- }
- private InsightManager Manager { get; set; }
- public bool IsCaseSubmitted
- {
- get;
- private set;
- }
- public Exception FailureException { get; set; }
- public CaseReport CreateCase()
- {
- return Manager.CreateCase();
- }
- public void SubmitCase(CaseReport caseReport)
- {
- try
- {
- Manager.SubmitCase(caseReport);
- IsCaseSubmitted = true;
- }
- catch (Exception ex)
- {
- FailureException = ex;
- IsCaseSubmitted = false;
- }
- }
- public class MockInsightManager : IInsightManagerBase
- {
- public MockInsightManager(bool submitCaseFails)
- {
- SubmitCaseFails = submitCaseFails;
- }
- public bool SubmitCaseFails { get; set; }
- public bool IsCaseSubmitted { get; set; }
- public CaseReport CreateCase()
- {
- return InsightManager.Current.CreateCase();
- }
- public void SubmitCase(CaseReport caseReport)
- {
- if (!SubmitCaseFails)
- {
- IsCaseSubmitted = true;
- }
- }
- }
So now we have a lot of the backup code, what does the actual view code in the controller look like? I’ll first show what it would look like if you just used the insight manager for those that want to see what it would look like when using Insight out of the box:
- [AcceptVerbs(HttpVerbs.Post)]
- public ActionResult Feedback(CaseReportDetail detail)
- {
- try
- {
- var report = InsightManager.Current.CreateCase();
- report.EmailAddress = detail.Email;
- report.Title = detail.Title;
- report.Description = detail.Description;
- report.CaseType = CaseType.Inquiry;
- InsightManager.Current.SubmitCase(report);
- return View("FeedbackReceived", detail);
- }
- catch
- {
- ModelState.AddModelError("Feedback", "There was a problem submitting the feedback. You may try to press submit again to try again. We apologize for the inconvenience.");
- return View();
- }
- }
With no real indirection above, now let’s look at the controller after I modified it for easier unit testing.
Here’s the top of the controller class where I have a constructor:
- public class HelpController : Controller
- {
- private IInsightManagerBase _insightManager { get; set; }
- public HelpController()
- : this(new InsightManagerWrapper())
- {
- }
- public HelpController(IInsightManagerBase insightManager)
- {
- _insightManager = insightManager;
- }
And here is the modified View method based on this new constructor:
- [AcceptVerbs(HttpVerbs.Post)]
- public ActionResult Feedback(CaseReportDetail detail)
- {
- var report = _insightManager.CreateCase();
- report.EmailAddress = detail.Email;
- report.Title = detail.Title;
- report.Description = detail.Description;
- report.CaseType = CaseType.Inquiry;
- _insightManager.SubmitCase(report);
- if (_insightManager.IsCaseSubmitted)
- {
- return View("FeedbackReceived", detail);
- }
- else
- {
- ModelState.AddModelError("Feedback", "There was a problem submitting the feedback. You may try to press submit again to try again. We apologize for the inconvenience.");
- return View();
- }
- }
The Negative Case
So now the original test works… but what happens if something goes wrong when submitting the case?
I added a new test that looks like this:
- [TestCategory("Build"), TestCategory("Unit"), TestMethod]
- public void Feedback_Not_Received()
- {
- // Arrange
- string expected = "Feedback"; // Did not work so we stayed on the Feedback view showing the error.
- string actual;
- var detail = new CaseReportDetail() { Description = "Test", Title = "Test" };
- var controller = CreateControllerAsNonAdmin(true);
- // Act
- var result = controller.Feedback(detail) as ViewResult;
- // Assert
- Assert.IsFalse(result.ViewData.ModelState.IsValid); // Was the model invalid?
- actual = result.ViewName;
- Assert.AreEqual(expected, actual); // Did it forward to the right view?
- }
Then when I was done, I had 100{f073afa9b3cad59b43edffc8236236232bb532d50165f68f2787a3c583ed137f} code coverage for the Feedback method!
I hope this helps others unit test the CodeSmith Insight…
Leave a Reply