About this topic, I have asked another question:
ASP.NET MVC Custom Route Constraints and Dependency Injection
Here is the current situation: on my ASP.NET MVC 3 App, I have a route constraint defined like below:
public class CountryRouteConstraint : IRouteConstraint {
private readonly ICountryRepository<Country> _countryRepo;
public CountryRouteConstraint(ICountryRepository<Country> countryRepo) {
_countryRepo = countryRepo;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
//do the database look-up here
//return the result according the value you got from DB
return true;
}
}
I am using this like below:
routes.MapRoute(
"Countries",
"countries/{country}",
new {
controller = "Countries",
action = "Index"
},
new {
country = new CountryRouteConstraint(
DependencyResolver.Current.GetService<ICountryRepository<Country>>()
)
}
);
At the unit testing part, I used the below code:
[Fact]
public void country_route_should_pass() {
var mockContext = new Mock<HttpContextBase>();
mockContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns("~/countries/italy");
var routes = new RouteCollection();
TugberkUgurlu.ReservationHub.Web.Routes.RegisterRoutes(routes);
RouteData routeData = routes.GetRouteData(mockContext.Object);
Assert.NotNull(routeData);
Assert.Equal("Countries", routeData.Values["controller"]);
Assert.Equal("Index", routeData.Values["action"]);
Assert.Equal("italy", routeData.Values["country"]);
}
Here, I cannot figure out how to pass the dependency. Any idea?
Personally I try and avoid performing such validation within a route constraint as it is much harder to express your intentions this way. Instead I use constraints to ensure parameters are in the correct format/type and put such logic in my controllers.
In your example I'm assuming that if the country is not valid then you will fall back to a different route (say to a "Country Not Found" page). Relying on your routing configuration is much less reliable (and much more likely to be broken) than accepting all country parameters and checking them in your controller:
public ActionResult Country(string country)
{
if (country == "france") // lookup to db here
{
// valid
return View();
}
// invalid
return RedirectToAction("NotFound");
}
That aside, what you are trying to achieve here (as has already been mentioned) is actually an integration test. When you find that parts of the framework are getting in the way of your tests then it could be time for a refactor. In your example I would want to test
The first thing we can do is move the Country validation into a separate class:
public interface ICountryValidator
{
bool IsValid(string country);
}
public class CountryValidator : ICountryValidator
{
public bool IsValid(string country)
{
// you'll probably want to access your db here
return true;
}
}
We can then test this as a unit:
[Test]
public void Country_validator_test()
{
var validator = new CountryValidator();
// Valid Country
Assert.IsTrue(validator.IsValid("france"));
// Invalid Country
Assert.IsFalse(validator.IsValid("england"));
}
Our CountryRouteConstraint
then changes to:
public class CountryRouteConstraint : IRouteConstraint
{
private readonly ICountryValidator countryValidator;
public CountryRouteConstraint(ICountryValidator countryValidator)
{
this.countryValidator = countryValidator;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
object country = null;
values.TryGetValue("country", out country);
return countryValidator.IsValid(country as string);
}
}
We map our route like so:
routes.MapRoute(
"Valid Country Route",
"countries/{country}",
new { controller = "Home", action = "Country" },
new { country = new CountryRouteConstraint(new CountryValidator())
});
Now if you really feel it is necessary to test the RouteConstraint you can test this independently:
[Test]
public void RouteContraint_test()
{
var constraint = new CountryRouteConstraint(new CountryValidator());
var testRoute = new Route("countries/{country}",
new RouteValueDictionary(new { controller = "Home", action = "Country" }),
new RouteValueDictionary(new { country = constraint }),
new MvcRouteHandler());
var match = constraint.Match(GetTestContext(), testRoute, "country",
new RouteValueDictionary(new { country = "france" }), RouteDirection.IncomingRequest);
Assert.IsTrue(match);
}
Personally I wouldn't bother performing this test as we've already abstracted the validation code so really this is just testing the framework.
To test the route mapping we can use MvcContrib's TestHelper.
[Test]
public void Valid_country_maps_to_country_route()
{
"~/countries/france".ShouldMapTo<HomeController>(x => x.Country("france"));
}
[Test]
public void Invalid_country_falls_back_to_default_route()
{
"~/countries/england".ShouldMapTo<HomeController>(x => x.Index());
}
Based on our routing configuration we can verify that a valid country maps to the country route and an invalid country maps to the fallback route.
However, the main point of your question was how to handle the dependencies of route constraints. The test above is actually testing a number of things - our routing configuration, route constraint, validator and probably access to a repository/database.
If you're relying on a IoC tool to inject these for you, you're going to have to mock your validator and repository/db and register these with your IoC tool in the set up of your tests.
It would be better if we could control how constraints are created:
public interface IRouteConstraintFactory
{
IRouteConstraint Create<TRouteConstraint>()
where TRouteConstraint : IRouteConstraint;
}
Your "real" implementation can just use your IoC tool to create the IRouteConstraint
instance.
I like to put my routing configuration in a separate class like so:
public interface IRouteRegistry
{
void RegisterRoutes(RouteCollection routes);
}
public class MyRouteRegistry : IRouteRegistry
{
private readonly IRouteConstraintFactory routeConstraintFactory;
public MyRouteRegistry(IRouteConstraintFactory routeConstraintFactory)
{
this.routeConstraintFactory = routeConstraintFactory;
}
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Valid Country",
"countries/{country}",
new { controller = "Home", action = "Country" },
new { country = routeConstraintFactory.Create<CountryRouteConstraint>() });
routes.MapRoute("Invalid Country",
"countries/{country}",
new { controller = "Home", action = "index" });
}
}
Constraints with external dependencies can be created using the factory.
This makes testing much easier. Since we're only interested in testing the country routes we can create a test factory that does only what we need:
private class TestRouteConstraintFactory : IRouteConstraintFactory
{
public IRouteConstraint Create<TRouteConstraint>() where TRouteConstraint : IRouteConstraint
{
return new CountryRouteConstraint(new FakeCountryValidator());
}
}
Note that this time we're using a FakeCountryValidator
that contains just enough logic for us to test our routes:
public class FakeCountryValidator : ICountryValidator
{
public bool IsValid(string country)
{
return country.Equals("france", StringComparison.InvariantCultureIgnoreCase);
}
}
When we set up our tests we pass the TestRouteFactoryConstraint
to our route registry:
[SetUp]
public void SetUp()
{
new MyRouteRegistry(new TestRouteConstraintFactory()).RegisterRoutes(RouteTable.Routes);
}
This time when we run our routing tests we're not testing our validation logic or database access. Instead we are unit testing our routing configuration when either a valid or invalid country is provided.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With