Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does my asp.net mvc controller not call it's base class' Initialize method during unit tests?

I am attempting to unit test my controllers, and I am using the default MVC 3 AccountController unit tests as a basis. So far I have my controller, which looks like:

public partial class HomeController : MyBaseController
{
    public HomeController(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; }

    public virtual ActionResult Index()
    {

        return View();
    }

    public virtual ActionResult About()
    {
        return View();
    }
}

MyBaseController has the following code:

public class MyBaseController : Controller
{
    public MyJobLeadsBaseController()
    {
        CurrentUserId = 0;
    }

    protected override void Initialize(RequestContext requestContext)
    {
        if (MembershipService == null) { MembershipService = new AccountMembershipService(); }
        base.Initialize(requestContext);

        // If the user is logged in, retrieve their login id
        var user = MembershipService.GetUser();
        if (user != null)
            CurrentUserId = (int)user.ProviderUserKey;
    }

    public int CurrentUserId { get; set; }
    public IMembershipService MembershipService { get; set; }

    protected IUnitOfWork _unitOfWork;
}

Everything works correctly when I run the actual site, and breakpoints show that the Initialize() is correctly being triggered. However, the following unit test never runs the Initialize(RequestContext) method:

    [TestMethod]
    public void Index_Shows_When_Not_Logged_In()
    {
        // Setup
        HomeController controller = new HomeController(_unitOfWork);
        controller.MembershipService = new MockMembershipService(null);
        SetupController(controller);

        // Act
        ActionResult result = controller.Index();

        // Verify
        Assert.IsNotNull(result, "Index returned a null action result");
        Assert.IsInstanceOfType(result, typeof(ViewResult), "Index did not return a view result");
    }

    protected static void SetupController(Controller controller)
    {
        RequestContext requestContext = new RequestContext(new MockHttpContext(), new RouteData());
        controller.Url = new UrlHelper(requestContext);

        controller.ControllerContext = new ControllerContext
        {
            Controller = controller,
            RequestContext = requestContext
        };
    }

Debugging through this unit test shows that at no point does the overridden MyBaseController.Initialize() get called at all. This causes issues where my CurrentUserId property is not being set in unit tests, but is being set on the live system.

What else do I have to do to trigger the Initialize() to be called?

like image 479
KallDrexx Avatar asked Dec 13 '22 14:12

KallDrexx


2 Answers

When you make a request to a controller through a website the MVC framework picks up that request and runs it through a number of different steps. Somewhere in that pipeline of steps MVC knows that it must call the Initialize method so it finds the Initialize in your MyBaseController class and executes it. At this point all is well and everything works.

When you create a new instance of your HomeController in your test code you're simply creating a new instance of a class. You're not running that class through the MVC request pipeline and your test framework doesn’t know to execute the Initialize method so you get an error.

Personally I would look to test the Index action independently of the Initialize method. Your Index action above is empty so I can't see exactly what you're trying to do but if you are returning one view for logged in users and another for anonymous users I'd do something like this:

[TestMethod]
public void Index_Shows_When_Not_Logged_In(){
    HomeController controller = new HomeController(_unitOfWork);
    controller.CurrentUserId=0;

    controller.Index();

    //Check your view rendered in here
}

[TestMethod]
public void Some_Other_View_Shows_When_Logged_In(){
    HomeController controller = new HomeController(_unitOfWork);
    controller.CurrentUserId=12; //Could be any value

    controller.Index();

    //Check your view rendered in here
}

There are some pretty nifty test helpers in the MVC contrib project (http://mvccontrib.codeplex.com/) which allow you to do things like:

controller.Index().AssertViewRendered();
like image 84
AndyM Avatar answered Dec 28 '22 07:12

AndyM


The constructor does not call Initialize. IIRC What calls initialize is the Execute/ExecuteCore method.

Here is the code from the MVC source:

protected virtual void Execute(RequestContext requestContext) {
            if (requestContext == null) {
                throw new ArgumentNullException("requestContext");
            }
            if (requestContext.HttpContext == null) {
                throw new ArgumentException(MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, "requestContext");
            }

            VerifyExecuteCalledOnce();
            Initialize(requestContext);

            using (ScopeStorage.CreateTransientScope()) {
                ExecuteCore();
            }
        }

This is basically called from the MvcHandler in the BeginProcessRequest method.

Update:

RequestContext does not exist in a unit test, because you're not going through ASP.NET. If I remember correctly you'd need to mock it.

Another problem in using Initialize in a test is the membership provider, which I haven't used myself, but I would guess that AccountMembershipService would fail you in a test? It seems to me that this would create fragile tests. It would probably also slow you down if it has to contact a server, and might fail you on a CI server.

IMO,from a basic look around the Init method, it shouldn't be there. The easiest solution,without breaking anything, that comes to mind is using dependency injection to inject the CurrentUserId in the Controller ctor.

like image 40
Linkgoron Avatar answered Dec 28 '22 05:12

Linkgoron