2

I have an Area in my MVC site. This area has the typical Controller/Model/View setup.

As a controller I have the following code:

public class DocumentCreatorController : Controller
{
    // GET: Templates/DocumentCreator
    public ActionResult OfferTemplate(BaseDocumentViewModel data)
    {
        return this.Pdf(nameof(OfferTemplate), data, "File.pdf");
    }
}

The method this.Pdf does a couple of stuff, but the interesting is it comes down to the ViewEngine call:

var viewResult = ViewEngines.Engines.FindPartialView(controllerContext, partialViewName);

Here I call the FindPartialView with a ControllerContext and a PartialViewName. My PartialViewName comes from the nameof(OfferTemplate) from the controller action OfferTemplate. I think the controllercontext is my challenge.

My challenge:

When I want to set this up in a unit test (using Moq), I have the following code based on pages such as Mocking The RouteData Class in System.Web.Routing for MVC applications and Mocking Asp.net-mvc Controller Context:

[TestMethod]
public void OfferTemplate()
{
    var ctr = SetupControllerWithContext();

}

private static DocumentCreatorController SetupControllerWithContext()
{
    var routeData = new RouteData();
    routeData.Values.Add("controller", "DocumentCreatorController");
    routeData.Values.Add("action", "OfferTemplate");


    var request = new Mock<HttpRequestBase>();
    request.Expect(r => r.HttpMethod).Returns("GET");
    var mockHttpContext = new Mock<HttpContextBase>();
    mockHttpContext.Expect(c => c.Request).Returns(request.Object);
    var controllerContext = new ControllerContext(mockHttpContext.Object
        , routeData, new Mock<ControllerBase>().Object);

    DocumentCreatorController ctr = new DocumentCreatorController();
    ctr.ControllerContext = controllerContext;
    return ctr;
}

Which gives the following error:

Eesy.Websites.Api.Tests.Controllers.DocumentCreatorControllerTest.OfferTemplate threw exception: System.NullReferenceException: Object reference not set to an instance of an object.

This I don't understand.

My folder setup:

enter image description here

Debug image on ControllerContext on calling the FindPartialView:

enter image description here

Anyone have an idea? Is it because I setup the RouteData wrong?

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Lars Holdgaard
  • 9,496
  • 26
  • 102
  • 182
  • 1
    It would need to be `routeData.Values.Add("controller", "DocumentCreator");` (not `"DocumentCreatorController") –  Aug 15 '17 at 11:31
  • 1
    You are trying to mock and test framework code. Abstract that functionality out into code you control so you can test in isolation if needed. This appears to be an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). What is the ultimate goal you are trying to achieve? – Nkosi Aug 15 '17 at 11:34
  • @Nkosi Good point. I am trying to auto generate a PDF (HTML to PDF) using a unit test, so I can verify the PDF generation proces works. And I hoped to do this using my controller, but obviously, it could make sense to make a service instead to avoid all this :-) – Lars Holdgaard Aug 15 '17 at 11:39
  • @StephenMuecke And you're of course right. I updated it - but also a null reference error – Lars Holdgaard Aug 15 '17 at 11:39

1 Answers1

1

You are trying to mock and test framework code. Abstract that functionality out into code you control so you can test in isolation if needed.

Currently the action and by extension the controller is tightly coupled to external 3rd party dependencies. If the goal was to test the controller action flow in isolation then it is advised to abstract out the 3rd party PDF generation so that it can be mocked for easier testability.

public interface IDocumentService {
    ActionResult ToPdf(Controller arg1, string arg2, object arg3, string arg4);
}

The controller would explicitly depend on this abstraction via constructor injection.

public class DocumentCreatorController : Controller {
    private readonly IDocumentService render;

    DocumentCreatorController(IDocumentService render) {
        this.render = render;
    }

    // GET: Templates/DocumentCreator
    public ActionResult OfferTemplate(BaseDocumentViewModel data) {
        return render.ToPdf(this, nameof(OfferTemplate), data, "File.pdf");
    }
}

So now to test the controller's pdf generation process you need only mock your abstraction.

[TestMethod]
public void OfferTemplate() {
    //Arrange    
    var serviceMock = new Mock<IDocumentService>();
    //...setup mock for use case

    var controller = new DocumentCreatorController(serviceMock.Object);
    var data = new BaseDocumentViewModel {
        //...
    };

    //Act
    var actual = controller.OfferTemplate(data);

    //Assert
    //...assert behavior
}

The actual implementation of the service would encapsulate the actual functionality and would be registered with the dependency injection container along with the abstraction.

To test the actual generation you would need to do an integration test which is another topic.

Nkosi
  • 235,767
  • 35
  • 427
  • 472