views:

79

answers:

6

I have a controller that overrides OnActionExecuting and does something like this:

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    base.OnActionExecuting(filterContext);
    string tenantDomain = filterContext.RouteData.Values["tenantDomain"] as string;
    if (!string.IsNullOrWhiteSpace(tenantDomain))
    {
        using (var tx = BeginTransaction())
        {
            this.Tenant = repo.FindOne(t => t.Domain == tenantDomain);
        }
    }
}

Tenant is a protected property with a private setter. The class itself is an abstract base controller that my real controllers derive from. I have code in other controllers that looks a lot like this:

if (Tenant == null)
{
   // Do something
}
else
{
   // Do something else
}

How do I test this code? What I need to do is to somehow set the Tenant property, but I can't because:

  1. It's a protected property, and
  2. It has a private setter

Changing the visibility of Tenant doesn't "feel" right. What are my alternatives to unit test my derived controllers?

A: 

Using reflection?

Something like this should work (not tested):

  YourController.GetType()
    .GetProperty("Tenant", BindingFlags.NonPublic | BindingFlags.Instance)
    .SetValue(yourControllerInstance, null, theTestValue);
Koen
Using a Private Accessor Assembly would be significantly easier than writing the reflection code your self.
driis
Unless you copy/paste/slightly adapt the above code ;) My point is: you're right that it's not a bad thing to create mocks or dummy code (because it's outside of the production code), and the same goes for a reflection solution (which you probably should refactor for re-use).
Koen
This seems to be the cleanest way to do it. I have a feeling that even if I go with the TenantFinder approach suggested by Ryan, I would still need to use reflection to invoke the OnActionExecuting method when I'm testing my derived controllers.
Ragesh
+1  A: 

Here's how I would do it:

Create a concrete class in your test project, that inherits from your abstract controller, and exposes Tenant as a public property. There is nothing wrong with having mock or dummy code in the test project (and of course, dummy code that just facilitates testing, should never be in the production projects).

If that is not a good fit for your situation; you could use Visual Studio's Private Accessor Assembly feature to generate a type where you can access your Tenant property. It is available in the IDE, or in command-line form.

driis
But then the controller he's testing would have to inherit from that.
Ryan
@Ryan, as I read the question, he wants to test the abstract controller code ? If that is not the case, then he could make a mock of the controller that he is testing.
driis
@driis I read it as two separate questions. You are correct for testing the abstract code. He also asks how to test his `if(Tenant == null)` code inside the actual controllers. For that he will need a way to set Tenant. I suppose he could stub some route data and call `OnActionExecuting` in each test, but it's not as explicit (setup-wise) as an internal method in my opinion.
Ryan
See my second answer below for a very explicit solution.
Ryan
Is this a normal practice? I can see why it's required for testing abstract classes, but when I'm testing my concrete controllers, I'd prefer to test them "as is", instead of deriving from them and testing the derived classes.
Ragesh
A: 

You could create a method on the abstract base for use only in unit testing. You can either make it public or internal and set [assembly: InternalsVisibleTo("Tests")] on the MVC project like this.

internal void SetTenant(Tenant testValue)
{
    Tenant = testValue;
}
Ryan
I am not very fond of adding anything to the implementation just to support unit tests. If that SetTenant method gets added, someone will come along and misuse it at some point in time. It breaks incapsulation.
driis
I agree with driis here. Altering the visibility of fields just to make it testable isn't something I'd like to do.
Ragesh
A: 

Hi I understand that you want to test the concrete controllers that inherits from your base class. It looks like you want want to stub your repo to return the tenant corresponding to the context of your test.
Your need:

What I need to do is to somehow set the Tenant property

And from your implementation the tenant is provide by the repo

this.Tenant = repo.FindOne(t => t.Domain == tenantDomain);

What you need is to be able to inject your repo by some way and from the name "repo" that sounds like an ok thing.
does that make sense?

-- Dom

Dom Ribaut
Ryan cleaned the code in the abstract class that would have failed the following test "OnActionExecuting should get the Tenant for the tenantDomain in the filterContext from the TenantService". Test would have failed because of `if (!string.IsNullOrWhiteSpace(tenantDomain))`And his NullTenant object is definitely a good idee that will remove the if tenant == null
Dom Ribaut
I really don't have a need to stub my repository. I'm stubbing my database in the tests, so the repository works just fine.
Ragesh
Then you could set up filterContext.RouteData.Values["tenantDomain"] with "right" values but the point of stubing the repo is to have control on what is returned by 'repo.FindOne(t => t.Domain == tenantDomain);' in the test and to do it as near from the test as possible. For testing your use case you could have 2 small stubs repoStubReturnsMyDomain and repoStubReturnsNull that always deliver predictable data.
Dom Ribaut
A: 

Ok after re-reading @driis answer I think there is a more explicit way to do this but it will require using DI in your controller factory. (There are many samples of how to do this online.)

First we are going to encapsulate the logic of how to get a Tenant from a route parameter.

public class TenantFinder : ITenantFinder
{
    // todo DI for repo

    public Tenant Find(string tenantDomain)
    {
        if (string.IsNullOrWhiteSpace(tenantDomain))
        {
            return null;
        }
        using (var tx = BeginTransaction())
        {
            return repo.FindOne(t => t.Domain == tenantDomain);
        }
    }
}

Then we can inject this class into the controller. (Icky part is putting the constructor parameter everywhere!) So in the abstract controller:

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    base.OnActionExecuting(filterContext);
    string tenantDomain = filterContext.RouteData.Values["tenantDomain"] as string;
    Tenant = Finder.Find(tenantDomain );
}

Then in your unit test you can create a stub of ITenantFinder to inject the tenant you want to use. You could even make a fake class like NullTenantFinder to handle the null cases.

Ryan
I like this approach. I already have DI wired up for my controllers so this would be easy to adopt, but how would I call OnActionExecuting when I'm testing my concrete controllers? Reflection?
Ragesh
On the other hand, this is actually a way to mock my repository when I'm testing, right? The thing is that my repository works just fine because I'm stubbing the database.
Ragesh
In this case I'd recommend using reflection to call it. You could create an extension method for your base controller class to do this. I love keeping my test code declarative so I'd do something like `controller.SetupFor(testTenant)` and `controller.SetupForNullTenant()`.
Ryan
+1  A: 

Hi Ragesh,

There are two ways to accomplish this

  1. Mock the Tenant Property (Standard Way)

    Mock<TenantType> tenantMock = new Mock<TenantType>();    
    var controller = new TargetController();    
    Controller.Tenant = tenantMock.Object;
    
  2. Use reflection to set the private property (for those who are not familiar with mocking frameworks)

E.g.,

    private static void SetPrivateSetPropertyValue(Type type, object obj, string fieldName, object value)
    {
        PropertyInfo propInfo = type.GetProperty(fieldName);
        propInfo.SetValue(obj, value, null);
    }

Consuming Code

this.SetPrivateSetPropertyValue(TenantType.GetType(),controller , "Tenant",tenantInstance);

Hope this helps,

Thanks , Vijay.

vijaysylvester