Over the last few months I’ve been helping develop an Appium/WinAppDriver framework (or library if you prefer) to allow one of the application to look to move one of my client’s application away from CodedUI (support is being phased out for this).
One of the problem I had is, I wanted to be able to put text into the Windows Clipboard and paste it into an various edit controls. The pasting made things slightly quicker but also, more importantly, bypassed some control’s autocomplete which occasionally messes up using SendKeys.
The problem is that the clipboard uses OLE and NUnit complains that it needs to be running in an STA.
To fix this, SpecFlow classes are partial so we can create new classes within another file with the same classname (and mark as partial) and namespace and then add the NUnit require attribute as below
[NUnit.Framework.Apartment(System.Threading.ApartmentState.STA)] public partial class CalculatorFeature { }
This is all well and good. It’s hardly a big deal, but we have hundreds of classes and it’s just more hassle, remembering each time to do this, or you run the tests and when it hits a Paste from the Clipboard, it fails, wasting a lot of time before you’re reminded you need to create the partial class.
SpecFlow has the ability to intercept parameters passed into our steps, it has tags and hooks which allow you to customize behaviour and it also comes with a way to interact with the code generation process which we can use to solve this issue and generate the required attributes in the generated code.
The easiest way to get started is follow the steps below, although if you prefer, the code I’m presenting here is available on my PutridParrot.SpecFlow.NUnitSta GitHub repo., so you can bypass this post and go straight the code for this article if you prefer.
- Grab the zip or clone the repo to get the following GenerateOnlyPlugin
- You may want to update SampleGeneratorPlugin.csproj to
<TargetFrameworks>net472;netcoreapp3.1</TargetFrameworks>
I would keep to these two frameworks to begin with, as we have a few other places to change and it’s better to get something working before we start messing with the rest of the build properties etc.
- In the build folder edit the .targets file to reflect the Core and !Core frameworks, i.e.
<_SampleGeneratorPluginFramework Condition=" '$(MSBuildRuntimeType)' == 'Core'">netcoreapp3.1</_SampleGeneratorPluginFramework> <_SampleGeneratorPluginFramework Condition=" '$(MSBuildRuntimeType)' != 'Core'">net472</_SampleGeneratorPluginFramework>
Again I’ve left _SampleGeneratorPluginFramework for now as I just want to get things working.
- Next go to the .nuspec file and change the file location at the bottom of this file, i.e.
<file src="bin\$config$\net472\PutridParrot.SpecFlow.NUnitSta.*" target="build\net472"/> <file src="bin\$config$\netcoreapp3.1\PutridParrot.SpecFlow.NUnitSta.dll" target="build\netcoreapp3.1"/> <file src="bin\$config$\netcoreapp3.1\PutridParrot.SpecFlow.NUnitSta.pdb" target="build\netcoreapp3.1"/>
- Now go and change the name of the solution, the project names and the assembly namespace if you’d like to. Ofcourse this also means changing the PutridParrot.SpecFlow.NUnitSta in the above to your assembly name.
At this point you have a SpecFlow plugin which does nothing, but builds to a NuGet package and in your application you can set up your nuget.config with a local package pointing to the build from this plugin. Something like
<packageSources> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> <add key="Local" value="F:\Dev\PutridParrot.SpecFlow.NUnitSta\PutridParrot.SpecFlow.NUnitSta\bin\Debug"/> </packageSources>
Obviously change the Local package repository value to the location of your package builds.
Now if you like, you can create a default SpecFlow library using the default SpecFlow templates. Ensure the SpecFlow.NUnit version is compatible with your plugin (so you may need to update the package from the template one’s). Finally just add the package you built and then build your SpecFlow test library.
If all went well, nothing will have happened, or more importantly, no errors will be displayed.
Back to our Plugin, thankfully I found some code to demonstrated adding global:: to the NUnit attributes on GitHub, so my thanks to joebuschmann.
- Create yourself a class, mine’s NUnit3StaGeneratorProvider
- This should implement the interface IUnitTestGeneratorProvider
- The constructor should look like this (along with the readonly field)
private readonly IUnitTestGeneratorProvider _unitTestGeneratorProvider; public NUnit3StaGeneratorProvider(CodeDomHelper codeDomHelper) { _unitTestGeneratorProvider = new NUnit3TestGeneratorProvider(codeDomHelper); }
- We’re only interested in the SetTestClass, which looks like this
public void SetTestClass(TestClassGenerationContext generationContext, string featureTitle, string featureDescription) { _unitTestGeneratorProvider.SetTestClass(generationContext, featureTitle, featureDescription); var codeFieldReference = new CodeFieldReferenceExpression( new CodeTypeReferenceExpression(typeof(ApartmentState)), "STA"); var codeAttributeDeclaration = new CodeAttributeDeclaration("NUnit.Framework.Apartment", new CodeAttributeArgument(codeFieldReference)); generationContext.TestClass.CustomAttributes.Add(codeAttributeDeclaration); }
All other methods will just have calls to the _unitTestGeneratorProvider method that matches their method name.
- We cannot actually use this until we register out new class with the generator plugins, so in your Plugin class, mine’s NUnitStaPlugin, change the Initialize to look like this
public void Initialize(GeneratorPluginEvents generatorPluginEvents, GeneratorPluginParameters generatorPluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { generatorPluginEvents.CustomizeDependencies += (sender, args) => { args.ObjectContainer .RegisterTypeAs<NUnit3StaGeneratorProvider, IUnitTestGeneratorProvider>(); }; }
- If you changed the name of your plugin class ensure the assembly:GeneratorPlugin reflects this new name, the solution will fail to build if you haven’t updated this anyway.
Once all these things are completed, you can build your NuGet package again. It might be worth incrementing the version and then update in your SpecFlow test class library. Rebuild that and the generated .feature.cs file should have classes with the [NUnit.Framework.Apartment(System.Threading.ApartmentState.STA)] attribute now. No more writing partial classes by hand.
This plugin is very simple, all we’re really doing is creating a really minimal implementation of IUnitTestGeneratorProvider which just adds an attribute to a TestClass and we registered this with the GeneratorPluginEvents, there’s a lot more why could potentially do.
Whilst this was very simple in the end. The main issues I had previously with this are that we need to either copy/clone or handle these build props and targets as well as use .nuspec to get our project packaged in a way that works with SpecFlow.