So, you're like me and have a greater than 500 line T4 template that is a steaming pile of... code. And of course, no syntax highlighting without addons, no intellisense, generally horrible Visual Studio support, and near impossible to unit test.

Well, my friends, I have just the thing for you! After beating my head against a wall for several days, I've found salvation!

To use my method requires some "neat" code, and it requires a few assumptions:

You're using T4 to generate C# code

You're then taking this C# code and including it in your project

You're ready to do some major refactoring for an awesome experience in the end (and your T4 template will be so much cleaner afterwards!)

You can have at least 2 tiles tied to T4 (one T4 template and one include file)

The corner stone of making T4 testable is to separate out the "logic" and the "content"; where the content is the generated C# code, and the logic is what you do to generate it.

To do this and enforce this separation cleanly, you must have two files. One of these files is the T4 "view", and another file is the logic, which is capable of being normally compiled outside of T4.

Example Untestable T4 Code

Lets start with a simple example. You have a simple T4 template which takes a file like so:

Run an integration test manually comparing the code (very very brittle)

I can't think of anything else worth mentioning

The Great Refactor

Now, What I suggest:

Let's refactor this and make it so we can eliminate some logic out of our "view". In this case, the view should worry about getting the code into the generated file and that's it. The logic on the other hand should work at a level of abstraction.

Here's what we're going to do:

Eliminate most of this code from the view

Create a new file for the logic

Use a very clever trick so that our logic file will compile outside of T4 and work when included in T4

Make it so that instead of just outputting text, we're building an object model that happens to be easily translatable to text

Because I came prepared, I already have the object model abstraction built. you can catch it at the bottom of the last code snippet. (It's easy to rip out and put elsewhere)

Compiling Code In and Out of T4

So, we create a new T4 logic file and name it something like "GenerateClassLogic.tt.cs". Now, you may be thinking "but you can't use the same code in T4 and a regular project!" *WRONG!

I came across a very nifty trick. Behold:

//<#+
/*That line above is very carefully constructed to be awesome and make it so this works!*/
#if NOT_IN_T4
//Apparently T4 places classes into another class, making namespaces impossible
namespace MyNamespace.Foo.Bar
{
using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.IO;
#endif
//regular ol' C# classes and code...
#if NOT_IN_T4
} //end the namespace
#endif
//#>

The key parts are the first and last lines. These begin with a comment so that the C# compiler will ignore them outside of T4, but inside of T4 these instruct the transformer to include this as a "class feature" (which ends up being a nested class)

Note here also that you must define a compile symbol. This is super easy to do. If you add it to your project, then it won't carry over to the T4 template though, making this an easy way to add in a few key things that can't be done without knowing if we're executing within T4 or not.

So, now we have a T4 logic file that will compile inside and outside of T4. Perfect for unit testing! All we need now is some logic!

Conclusion and Remarks

So, in conclusion, we've learned that T4 is actually capable of taming. (when I first learned T4, I wouldn't have thought it possible either) The main thing to do is maintain a separation of "content" and logic. Now of course, there are some gotchas to watch for:

This only works when your T4 generator's target is to generate C# code

When you add a reference to something, you must add it to both the logic file and the view

It's still very difficult to reference external assemblies(particularly project assemblies). This doesn't solve that problem at all

You must define a compiler symbol for your project if you wish to run unit tests against it.

When other people use your T4 template outside of your project(and thus don't need to test it), they must ensure that the logic file is not compiled outside of the T4

Unit testing that a piece of code is generated is very difficult and brittle(hence the need for abstractions where possible to make this easier)

My abstraction mechanisms for building objects aren't really that good. They're good enough for me, but please someone improve them! (if you do it I'll link to you from here)