Unit Testing with MvcRouteTester.Mvc5.2, WebConfigTransformRunner and Nuget.Core
After researching this, I understand why there are no articles entitled “Unit Testing with WebConfigTransformRunner
.” This NuGet package contains an *.exe
: it is not meant to be referenced in Visual Studio as a Class library. To make this matter even more challenging, Phil Haack and his crew have very little documentation on NuGet.Core
—and getting Anthony Steele’s MVC Route Tester in play was no walk in park either.
So let me state the original desire, since the previous paragraph flippantly starts off somewhere in the middle: I want to unit-test XML document transforms (XDT) on my Web.config
files and I want to test attribute-based MVC routes. I also want to use Visual Studio Test (vstest.console.exe
) projects so automated-testing hipsters can howl at me with derisive laughter.
Testing with MvcRouteTester.Mvc5.2
Speaking of ease, my ShouldRouteToIndexController()
Visual Studio Test method shows that I can test routes with just three lines of code:
[TestMethod]
[TestProperty("jsonPath", @"Songhay.Blog.Tests\ShouldRouteToIndexController.json")]
public void ShouldRouteToIndexController()
{
var jsonPath = this.TestContext.Properties["jsonPath"].ToString();
jsonPath = Path.Combine(this.TestContext.ShouldGetProjectsFolder(this.GetType()), jsonPath);
this.TestContext.ShouldTestRoutes(typeof(IndexController), jsonPath);
}
Some programmers like to brag about so few lines of code—and then we like to go further and proclaim it’s really just one line of code. In my example above, TestContext.ShouldTestRoutes()
, an extension method I wrote, is really doing the test—all the other stuff is there to serve this single line. The intention is to load ShouldRouteToIndexController.json
into TestContext.ShouldTestRoutes()
, making this test very reusable and very data driven. The JSON data look like this:
[
{
"route": "/",
"controller": "Index",
"action": "Index",
"isNonRoute": false
},
{
"route": "/entry/show/FOO",
"controller": "Index",
"action": "RedirectToAngularSeed",
"isNonRoute": false
},
{
"route": "/entry/show",
"controller": "Index",
"action": null,
"isNonRoute": true
}
]
These data are loaded into that big one-liner. I’ve the GitHub Gist of TestContext.ShouldTestRoutes()
:
My Gist would not be possible without Anthony Steele’s MvcRouteTester.Mvc5.2. It was a bit of an ordeal to realize that I needed this package because there are four different versions of MVC Route Tester. Anthony himself is quite irritated by this as he as to compile multiple versions of MVC Route Tester for every major version of ASP.NET MVC:
The whole thing of needing a new build for each MVC version is a pain. We are in need of a very different approach...
Testing with WebConfigTransformRunner and Nuget.Core
Eric Hexter gives us WebConfigTransformRunner
as an *.exe
via a NuGet package. Clearly this package is optimized for the command line. But I still wanted to use it for automated testing and clearly take my childish, one-line-of-code bragging rights. So I’ve wrapped up Eric’s runner in yet another reusable extension method TestContext.ShouldTransformWebConfig()
, pictured below:
Before we show the Gist of this extension method, let’s have a look inside the collapsed region test properties
:
I’ve written another extension method, ShouldGetNuGetPackageFile()
, calling on NuGet.Core
:
This extension method would not be possible without DefaultPackagePathResolver
in the NuGet.Core
package. It digs into the NuGet packages and finds the path to Hexter’s executable. This path fills the pathToWebConfigTransformRunnerExe
argument in the Gist of the ShouldTransformWebConfig()
extension method:
I understand how one can be overcome by my use of extension methods. In my extension method ShouldTransformWebConfig()
, we have—wow—another interesting extension method, StartProcessAndWaitForExit()
which actually runs Hexter’s executable. I should have written this years ago! Here’s the gist for StartProcessAndWaitForExit()
:
After WebConfigTransformRunner
runs, its output is tested with the contents of the xPaths
argument. This type of argument makes the testing, again, data driven. Inside that collapsed region, test properties
, my other, other extension method, ShouldLoadListOfStrings()
(a simple wrapper for File.ReadAllLines(path)
), loads simple text file, ShouldTransformWebConfigXPaths.txt
, data-driving this test with these old-school XPath statements:
//system.webServer/staticContent/mimeMap[@fileExtension='.json']
//system.webServer/staticContent/mimeMap[@fileExtension='.opml']
Okay folks, one last extension method for today…
Do notice that none of these automated tests used absolute paths. All of the paths to set up the tests were relative. This is possible (in part) because I set up my Visual Studio projects folder like this:
[root]
\packages
\Project.Shared
\Project.Shared.Tests
\Project.One
\Project.One.Tests
\Project.Two
\Project.Two.Tests
Solution.One.sln
Solution.Two.sln
I am sure that Microsoft have very great reasoning around why Visual Studio prefers to wrap each *.sln
file in its own folder by default. But my intent is to share Project source code across multiple projects. Today I am under the impression that the best way to do this is with the folder layout above—so, for example, both solutions, Solution.One
and Solution.Two
, can easily share Project.Shared
.
This conventional folder layout leads to my last extension method for today, TestContext.ShouldGetProjectsFolder()
. It is used in both the tests mentioned in this article. This is a simple wrapper around one of my very old utility-class (or “helper” class) methods FrameworkAssemblyUtility.GetAssemblyDirectory()
(a simple wrapper around Path.GetDirectoryName(targetAssembly.Location)
):
/// <summary>
/// Test context extensions: should get projects folder.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="typeInAssembly">The type in assembly.</param>
public static string ShouldGetProjectsFolder(this TestContext context, Type typeInAssembly)
{
Assert.IsNotNull(typeInAssembly, "The expected type instance is not here.");
var assembly = typeInAssembly.Assembly;
var path = FrameworkAssemblyUtility.GetAssemblyDirectory(assembly);
path = path.Remove(path.IndexOf(typeInAssembly.Namespace));
context.ShouldFindFolder(path);
return path;
}
I am embarrassed to admit that it took me years to really trim the cognitive load to write something as straight-forward as ShouldGetProjectsFolder()
. It will be reused in all of my automated tests that depend on static files—I prefer this approach over the ceremony around Deployment Item setup.