Creating a Custom Unit Test Generator Provider Plugin for Specflow 1.9

2013-04-26

After getting CodedUI Code First and Specflow to play nicely together in the past week, we ran into a few issues with the distribution of the Custom Code Generator Provider assembly to all team members. The SpecFlow binaries installed under a path that contains a random character string and I felt a bit unhappy about the way that other developers had resolved this issue by placing the assembly in the NuGet package folder. Especially since we don't check in the binaries in TFS and package restore might go funcky.

The SpecFlow documentation mentions the new way of registering your Custom Generators through the new configuration item. That is where the documentation ends. But the promise of being able to store your plugin relatively to the test project root looked very promising so I got to work.There is now a SpecFlow.CustomPlugin NuGet Package which will get you started creating your own Specflow plugin. To add the package you must create a new Class Library project using .NET Framework 4.0 or 4.5.



Digging in the SpecFlow sources on GitHub I found a piece of code that helped me put together the IGeneratorPlugin implementation:

using TechTalk.SpecFlow.Infrastructure;

[assembly: GeneratorPlugin(typeof(CodedUIGeneratorProvider.Generator.SpecFlowPlugin.CodedUIGeneratorPlugin))]

namespace CodedUIGeneratorProvider.Generator.SpecFlowPlugin
{
    using System.CodeDom;

    using BoDi;

    using TechTalk.SpecFlow.Generator;
    using TechTalk.SpecFlow.Generator.Configuration;
    using TechTalk.SpecFlow.Generator.Plugins;
    using TechTalk.SpecFlow.Generator.UnitTestProvider;
    using TechTalk.SpecFlow.UnitTestProvider;
    using TechTalk.SpecFlow.Utils;

    /// <summary>
    /// The CodedUI generator plugin.
    /// </summary>
    public class CodedUIGeneratorPlugin : IGeneratorPlugin
    {
        /// <summary>
        /// The register dependencies.
        /// </summary>
        /// <param name="container">
        /// The container.
        /// </param>
        public void RegisterDependencies(ObjectContainer container)
        {
        }

        /// <summary>
        /// The register customizations.
        /// </summary>
        /// <param name="container">
        /// The container.
        /// </param>
        /// <param name="generatorConfiguration">
        /// The generator configuration.
        /// </param>
        public void RegisterCustomizations(ObjectContainer container, SpecFlowProjectConfiguration generatorConfiguration)
        {
            container.RegisterTypeAs<CodedUIGeneratorProvider, IUnitTestGeneratorProvider>();
            container.RegisterTypeAs<MsTest2010RuntimeProvider, IUnitTestRuntimeProvider>();
        }

        /// <summary>
        /// The register configuration defaults.
        /// </summary>
        /// <param name="specFlowConfiguration">
        /// The spec flow configuration.
        /// </param>
        public void RegisterConfigurationDefaults(SpecFlowProjectConfiguration specFlowConfiguration)
        {
        }
    }
...

The RegisterCustomizations method is used to setup the Dependency Injection configuration. I've used the same implementation for the CodedUIGeneratorProvider as used in so many samples out there:

...
    /// <summary>
    /// The CodedUI generator.
    /// </summary>
    public class CodedUIGeneratorProvider : MsTest2010GeneratorProvider
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="CodedUiGeneratorProvider"/> class.
        /// </summary>
        /// <param name="codeDomHelper">
        /// The code dom helper.
        /// </param>
        public CodedUIGeneratorProvider(CodeDomHelper codeDomHelper)
            : base(codeDomHelper)
        {
        }

        /// <summary>
        /// The set test class.
        /// </summary>
        /// <param name="generationContext">
        /// The generation context.
        /// </param>
        /// <param name="featureTitle">
        /// The feature title.
        /// </param>
        /// <param name="featureDescription">
        /// The feature description.
        /// </param>
        public override void SetTestClass(TestClassGenerationContext generationContext, string featureTitle, string featureDescription)
        {
            base.SetTestClass(generationContext, featureTitle, featureDescription);

            foreach (CodeAttributeDeclaration declaration in generationContext.TestClass.CustomAttributes)
            {
                if (declaration.Name == "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute")
                {
                    generationContext.TestClass.CustomAttributes.Remove(declaration);
                    break;
                }
            }

            generationContext.TestClass.CustomAttributes.Add(new CodeAttributeDeclaration(new CodeTypeReference("Microsoft.VisualStudio.TestTools.UITesting.CodedUITestAttribute")));
        }
    }
}

To register your plugin you can now use the following snippet of documentation, notice that it's much cleaner than the old style config:

<configuration>
  <configSections>
    <section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" />
  </configSections>
  <specFlow>
    <unitTestProvider name="MsTest" />
    <!-- For additional details on SpecFlow configuration options see http://go.specflow.org/doc-config -->
    <plugins>
      <add name="CodedUIGeneratorProvider" path=".\lib" type="Generator"/>
    </plugins>
  </specFlow>
</configuration>

To have Specflow load your plugin you must name it "{PluginName}.Generator.SpecflowPlugin.dll" or "{PluginName}.SpecflowPlugin.dll". The path property is always relative to the Test Project directory.

If your Custom Plugin requires additional dependencies to be injected for it to work, use the following syntax in the configuration file:

<configuration>
  <configSections>
    <section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" />
  </configSections>
  <specFlow>
    <!-- For additional details on SpecFlow configuration options see http://go.specflow.org/doc-config -->
    <plugins>
      <add name="CodedUIGeneratorProvider" path=".\lib" type="GeneratorAndRuntime"/>
    </plugins>
    <runtime>
      <dependencies>
        <register name="optional:Name" type="Namespace.Type, Assembly" as="Namespace.Interface, Assembly" />
      </dependencies>
    </runtime>
    <generator>
      <dependencies>
        <register name="optional:Name" type="Namespace.Type, Assembly" as="Namespace.Interface, Assembly" />
      </dependencies>
    </generator>
  </specFlow>
</configuration>

Note: There seems to be a bug with the Visual Studio test runner and the fact that the plugin isn't deployed to a place where the test runner can find it when you use the GeneratorAndRuntime option. The relative path in the config file no longer matches the location of the plugin, since the Visual Studio test runner uses the test deployment directory as the base directory. Using a combination of [Copy Always] and a [DeploymentItem] attribute might work, but I've not tried that yet.
 

Most Reading