Home =>  Articles =>  C#Wizardry: Code Generation With Class

C#Wizardry: Code Generation With Class
Charlie Poole
9/28/2002

Code Generation
Making It Run
What's It Good For

Aren't We There Yet?

In the first and second articles of this series, we developed a wizard in C# that could actually generate code and add it to our project! We took Windows Form-based input from the user to tell how it was to be generated. But our original goal was to do a bit more. We wanted to be able to generate a test fixture for a particular class in the project, with a test method for each public method of the class.

Figure 1 - New Wizard Form

Let's consider what we need to be able to do that. We'd like to get a list of all the classes in a project and display them on a form, so that the user can select one. In fact, since a Visual Studio solution may have more than one project, we'll need to get a list of projects as well. This will take us beyond a single input form, so we'll need a mechanism to sequence through the different forms both forward and - since it's expected of Wizards - backward. Finally, when we generate the code, we'll have to look at all the methods in a class and select the public methods for generating tests. We know that other things may come up along the way, but this list of tasks is enough to get us started.

A New Form

Let's start with the form. I decided to give the user the choice of generating a fixture for a specific class, one with a few sample tests or an empty fixture. Figure 1 shows the form I designed for this purpose.

The second form is the same Options form that we saw in the previous article, except now it's labeled as "Step 2 of 2."

Wizard Changes

Having two forms means the wizard class has to keep track of which one should be displayed at any given moment. Figure 2 shows the changes in this class from our earlier version.

The first thing you'll notice is that I changed the name of the class for my final version to be CSharpAddTestFixtureWizard. It's also now saving the value of the "application" parameter passed to Execute() in a private variable of type DTE. DTE stands for Development Tools Extensiblility and it's the top-most interface to the entire Visual Studio extension mechanism. The wizard uses this to retrieve the Solution object, which is a new read-only property it supports.

[ProgId("CSharpAddTestFixtureWiz")]
public class CSharpAddTestFixtureWiz : IDTWizard
{
    private DTE appDTE;

    private string wizardType;

    ...

    public Solution Solution
    {
    get { return appDTE.Solution; }
    }

    public void Execute ( object application , int hwndOwner , 
           ref object[] contextParams , ref object[] customParams , 
           ref EnvDTE.wizardResult retval )
    {
        appDTE = (DTE) application;
        wizardType = (string) contextParams[0];
			
        ...
		
        int step = 0;
        Form[] form = { new TestFixtureTypeForm( this ), 
                        new TestFixtureOptionsForm( this ) };

        while ( step >=0 && step < form.Length )
        {
            form[step].ShowDialog();

            switch( result )
            {
                case wizardResult.wizardResultSuccess:
                    ++step;
                    break;
                case wizardResult.wizardResultBackOut:
                    --step;
                    break;
                case wizardResult.wizardResultCancel:
                case wizardResult.wizardResultFailure:
                default:
                    retval = result;
                    return;
            }
        }

        if ( step > 0 )
            result = generator.Generate();

        retval = result;
    }
}
Figure 2 - Updated Wizard Code

The wizard now has code to determine which of a number of forms is to be displayed and to step forward and backward through those forms. To keep things as clean as possible, both forms are created right at the start. If the logic got more complicated than it currently is, we might need a separate object to handle this. But for two forms, this code seems to be the right level of specialization.

Projects And Classes

Our TestFixtureTypeForm - that's the name of our new form class - fills a combo box with a list of the projects in the current solution when it loads for the first time. Figure 3 shows the methods that do the job.

private void AddProjectsToCombo()
{
    int targetProjectIndex = -1;
    foreach ( Project project in wizard.Solution.Projects )
    {
        if ( HasTestableClasses( project ) )
        {
            int index = projectComboBox.Items.Add( project );
            if ( project.Name == wizard.ProjectName )
                targetProjectIndex = index;
        }
    }

    if ( targetProjectIndex >= 0 )
        projectComboBox.SelectedIndex = targetProjectIndex;
    else if ( projectComboBox.Items.Count > 0 )
        projectComboBox.SelectedIndex = 0;
}

private bool HasTestableClasses( Project project )
{
    try
    {
        return project.FileName != "" &&
            HasTestableClasses( project.CodeModel.CodeElements );
    }
    catch
    {
        // Ignore projects that throw an exception due
        // to language subsystem not being installed,
        // or being an unmodeled project type.
    }

    return false;
}
Figure 3 - Adding Projects to the ComboBox

We loop through all the project objects in the current solution, checking each of them to see if it has any classes for us to test. This eliminates things like install projects, for example. The project object itself is added to the combo box, which displays the name of the project by virtue of having its DisplayMember property set to Name. If the project into which we are adding the test fixture is placed on the list, we select it in the combo box. Note however, that it won't be in the list if it's a new empty project.

The HasTestableClasses method introduces a new concept. A Visual Studio Project object may have a CodeModel, which is a representation of all the code elements in the project. Using the code model, it's possible to find an element by it's qualified name, determine the language of the project and identify various subelements - as we are about to do. By the way, in this demonstration program, we are making the assumption that all the projects in a solution are written in C#. This is a pretty big assumption, since Visual Studio supports multiple-language solutions and a robust implementation of this wizard might need to deal with the possibility of other languages. For an explanation of some of the code elements we will be using, see the sidebar.

As we saw above, HasTestableClasses(Project) delegates its work to an overloaded method which takes a collection of CodeElements. In fact, the method has two additional overloads, both of which are shown in Figure 4.

The overload that takes a collection of code elements simply looks at each element to determine if it is a testable class or has one contained in it. Note that the only element which can contain a testable class is a namespace and that we check a namespace by looking at its members. The type of CodeNamespace's Members property is CodeElements, so this leads to another level of recursion.

private bool HasTestableClasses( CodeElements codeElements )
{
    foreach( CodeElement elem in codeElements )
    {
        if ( IsTestableClass( elem ) || HasTestableClasses( elem ) )
            return true;
    }

    return false;
}

private bool HasTestableClasses( CodeElement elem )
{
    if ( !IsNamespace( elem ) )
        return false;

    CodeNamespace codeNamespace = (CodeNamespace) elem;
    return HasTestableClasses( codeNamespace.Members );
}

private bool IsTestableClass( CodeElement elem )
{
    if ( !IsClass( elem) || !IsLocallyDefined( elem ) )
        return false;

    CodeClass codeClass = (CodeClass) elem;
    if ( codeClass.Access != vsCMAccess.vsCMAccessPublic )
        return false;

    foreach( CodeAttribute attribute in codeClass.Attributes )
        if ( attribute.Name == "TestFixture" )
            return false;

    return true;
}

private bool IsLocallyDefined( CodeElement elem )
{
    return elem.InfoLocation 
        == vsCMInfoLocation.vsCMInfoLocationProject;
}

private bool IsClass( CodeElement elem )
{
    return elem.Kind == vsCMElement.vsCMElementClass;
}

private bool IsNamespace( CodeElement elem )
{
    return elem.Kind == vsCMElement.vsCMElementNamespace;
}
Figure 4 - Checking for Testable Classes

To determine whether an element is a testable class, we check that it's a class and that it's locally defined. This is to avoid looking at any of the framework classes or other classes which are not defined in this project. We then convert the generic CodeElement object to a CodeClass so we can see if it has public access. Finally, we look at the attributes on the class for "TestFixture" to ensure that we don't try to test our test fixture classes.

Several small helper methods are used to encapsulate the checks we must make on CodeElements. It's very common in programming Visual Studio Wizards and Addins to have to make these checks before attempting to cast an object to its proper type.

We make extensive use of the same methods in adding testable classes to the second combo box each time a project is selected. The AddClassesToCombo method has two overloads which are shown in Figure 5. The first, taking a Project for its argument, simply calls on the second, passing in the CodeElements collection from that project's CodeModel.

private void AddClassesToCombo( Project project )
{
    classComboBox.Items.Clear();
    classComboBox.Text = "";

    if ( project != null )
        AddClassesToCombo( project.CodeModel.CodeElements );

    if ( classComboBox.Items.Count > 0 )
    classComboBox.SelectedIndex = 0;
}

private void AddClassesToCombo( CodeElements codeElements )
{
    foreach( CodeElement elem in codeElements )
    {
        if ( IsTestableClass( elem ) )
        {
            CodeClass codeClass = (CodeClass) elem;
            classComboBox.Items.Add( codeClass );
            AddClassesToCombo( codeClass.Members );
        }
        else if( IsNamespace( elem ) )
        {
            CodeNamespace codeNamespace = (CodeNamespace)elem;
            AddClassesToCombo( codeNamespace.Members );
        }
    }
}
Figure 5 - Adding Classes to the Second ComboBox

The overload that takes a CodeElements collection as its argument, examines each element and adds it to the combo if it's a testable class. It also looks at any nested classes to see if they should be added. If the element being examined is a namespace, then the method is called recursively on its members. As with the project combo, we insert the class object itself, relying on the DisplayMember property to show the name of the class for us.

Code Generation

Our code generator gets a few new fields and properties in this version. We use an enumeration to indicate which of the three types of code generation should be performed, and store the current value in a private member. A property exposes it to the outside world. Here's the enumeration:

public enum TestFixtureType
{
	ClassSpecificFixture,
	SampleFixture,
	EmptyFixture
}

Our original GenerateCSharpFile method generated hard-coded sample tests. In our new version, we use a switch statement to select one of three methods to be called depending on the type of test fixture being generated. InsertSampleTests() inserts the two sample methods we used in the previous version while InsertTestPlaceHolder() puts in a comment to indicate where the user should add tests.

GenerateCSharpFile()
{
    ...
	
    switch ( type )
    {
        case TestFixtureType.ClassSpecificFixture:
            InsertClassSpecificTests();
            break;
        case TestFixtureType.SampleFixture:
            InsertSampleTests();
            break;
        case TestFixtureType.EmptyFixture:
        default:
            InsertTestPlaceholder();
        break;
    }
	
    ....
}

void InsertSampleTests()
{
    InsertTestMethod( "SomeMethod" );
    InsertTestMethod( "AnotherMethod" );
}

void InsertTestPlaceholder()
{
    if ( nMethods > 0 )
        WriteLine();

    WriteLine( "//" );
    WriteLine( "// ToDo: Insert your tests here" );
    WriteLine( "//" );
}

void InsertClassSpecificTests()
{
    Hashtable names = new Hashtable();

    foreach( CodeElement elem in targetClass.Members )
    {
        if ( IsFunction( elem ) )
        {
            CodeFunction function = (CodeFunction) elem;
            if ( function.Access == vsCMAccess.vsCMAccessPublic
                 && !names.ContainsKey( function.Name ) )
            {
			    switch( function.FunctionKind )
                {
                    case vsCMFunction.vsCMFunctionConstructor:
                        InsertTestMethod( "Construction" );
                        break;
                    case vsCMFunction.vsCMFunctionFunction:
                        InsertTestMethod( function.Name );
                        break;
                    default:
                        break;
                }

                names.Add( function.Name, true );
            }
        }
    }
}

private bool IsFunction( CodeElement elem )
{
    return elem.Kind == vsCMElement.vsCMElementFunction;
}
Figure 6 - Generating Code

To generate tests for a selected class, we retrieve the CodeClass object from the combo box and examine all of it's members. The object model uses some very general terminology in it's code model, so the members we are interested in are of type CodeFunction. For each such public member, we look at the kind of "function" it represents. Most members are of the vsCMFunctionFunction type, and for them we generate a test. We also generate a test for the constructor using the name "Construction." A hashtable is used to ensure that we only generate one test for a given name, avoiding the need to worry about overloads.

Making It Run

To make this version run, I created a file called CSharpAddTestFixtureWiz.vsz and put it in my VC#\CSharpProjectItems directory. It contains these two lines.

   VSWIZARD 7.0
   Wizard="CSharpAddTestFixtureWiz"
   
In my Local Project Items directory I added a CSharpTestFixtureWiz.vsdir file, containing the following line (as usual, broken for readability).
   ..\CSharpAddTestFixtureWiz.vsz|0|Test Fixture|45|
   A class for use as an NUnit TestFixture|
   {FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}|4515|0|TestFixture.cs
   
This gives me a "Test Fixture" item under Local Project Items when I use the Add New Item dialog. As explained earlier you can make it appear in various locations by creating additional .vsdir files.

What's It Good For

I started this project with the notion that I wanted to do some sort of wizard that involved non-trivial interaction with the Visual Studio object model. That goal has been met quite well. I've learned a lot about extending Visual Studio and the techniques described will apply to other projects - like automatic refactoring of code.

But I also wanted to create a useful tool for generating test fixtures. So the question arises...

Is this a useful tool to have?

I have to answer that question with a qualified "Yes." If you need to generate test cases for a number of classes that don't now have tests, then this tool can help. It still lacks a number of features I'd like to see, like the ability to deal with multiple language solutions. But such features can always be added when they are needed, so I think this makes a good start toward a tool that could be used in real projects.

The real question is "Under what circumstances is it useful to have such a tool?" To that, I must say "Only rarely." In most cases, you'll be better served by writing one test at a time, before the corresponding application code is written. If you do that, you'll get along quite happily without this tool.

However, for legacy projects, you might consider something like this as a quick way of getting a large number of tests. In that case, I'd think about modifying the test generation so that the initial tests are flagged as non-runnable. This will give you a yellow bar and you'll have to actually add some code to see any tests pass at all. Of course, if you only want the illusion of tests, you can use it as is...

If you'd like to experiment with the wizard described here, the final source code can be downloaded here.