Recently whilst at a clients one of our consultants came across an interesting issue; the client was using Selenium to write web tests, they wanted to trigger them both from Microsoft Test Manager (MTM) as local automated tests, and also run them using BrowserStack for multi browser regression testing. The problem was to import the tests into MTM they needed to be written in MsTest and for BrowserStack nUnit.
As they did not want to duplicate each test what could they ?
After a bit of thought T4 templates came to the rescue, it was fairly easy to write a proof of concept T4 template to generate an MsTest wrapper for each nUnit at compile time. This is what we did, and the gotcha’s we discovered.
- Read the tutorial and resources on Oleg Sych’s blog on T4 Templating – a brilliant T4 resource
- Install the Visual Studio 2013 SDK
- Install the Visual Studio 2013 Modeling and Visualization SDK
[To make life easier this code has all been made available on GitHub]
- Create a solution containing a class library with some nUnit tests as test data
- Add a MsTest Unit Test project to this solution.
- Add a T4 ‘Text Template’ item to the MsTest project
- Write the T4 template that uses reflection to find the nUnit tests in the solution and generates the MsTest wrappers. See the source for the template on Github
- Once this is done both the nUnit and MsTest can now be run inside Visual Studio
- You can now add the tests to either MTM or BrowserStack as needed, each product using the unit tests it can see.
The Gotcha – you have two build engines
The main issues I had were due to me not realising the implications of the T4 template being processed in different ways between Visual Studio and MSBuild.
By default the template is processed whenever the .TT file is edited in Visual Studio, for me this is not the behaviour required, I wanted the template processed every time the nUnit tests are altered. The easiest way to do this is to always regenerate the .CS file from the template on a compile. Oleg again provides great documentation on how to do this, you end up editing the .CSPROJ file.
<!-- Include the T$ processing targets-->
<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />
<!-- Set parameters we want to access in the transform -->
<!-- Tell the MSBuild T4 task to make the property available: -->
<!-- do the transform -->
<!-- Force a complete reprocess -->
I thought after editing my .CSPROJ file to call the MSBuild targets required, and exposed properties I needed from MSBuild, that all would be good. However I quickly found that though when building my solution with MSBuild from the command line all was fine, a build in Visual Studio failed. Turns out I had to make my template support both forms of building.
This meant assuming in my .TT file I was building on MSBuild and if I got nulls for required property values switch to the Visual Studio way of working e.g.
// get the msbuild variables if we can
var configName = Host.ResolveParameterValue("-", "-", "configuration");
WriteLine ("// Generated from Visual Studio");
// Get the VS instance
IServiceProvider serviceProvider = (IServiceProvider)this.Host;
DTE dte = serviceProvider.GetService(typeof(DTE)) as DTE;
configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name ;
WriteLine ("// Generated from MSBuild");
Once this was done, I then made sure I could get a successful build both inside Visual Studio and from the command prompt in the folder containing my .SLN file (in my case passing in the Visual Studio version as I was using a VS2015RC command prompt, but only had the VS2013 SDKs installed) e.g.
So where are we now?
Now I have a nice little proof of concept on GitHub. To use it add the GeneratedMstests project to your solution and in this project add references to any nUnit projects. Once this is done you should be able to generate wrappers for nUnit tests.
I am sure I could do a better job of test discovery, adding references to assemblies and it would be a good idea to make my the sample code into a Visual Studio template, but it is a start, lets see if it actual does what is needed