Note: This post refers to an internal feature of the NUnit.Core library and cannot be guaranteed in future versions.
Yesterday I released my first open source project, Selenium Toolkit for .NET, if you’re into functional web-testing I highly recommend you check it out. I’d like to share a piece of code from that project – it’s either the biggest hack I’ve ever done or pretty clever. Let me know what you think.
I don't like manually doing things more than once if I have to. Ironically, while building a tool to help automate functional web testing, I found that I had to do a lot of manual verification despite all the unit tests that I had written for my components. It started to get pretty frustrating: make a small change to my addin, uninstall and reinstall it, then open and run my sample test-fixtures to see how the NUnit framework had interpreted my changes. If I was going anywhere with this project, this was going to slow me down.
Fortunately, while spelunking through the NUnit.Core library, I found a really useful utility class that the fit the bill nicely and worked out pretty well. The TestAssemblyBuilder uses the nunit internals to find all the tests in an assembly and put them into a single Suite for inspection, execution, etc.
namespace Example { using NUnit.Core.Builders; public class ExampleTest { [Test] public void CanGetTestCount() { TestAssemblyBuilder assemblyBuilder = new TestAssemblyBuilder(); TestSuite suite = assemblyBuilder.Build("AssemblyName.dll", false); Assert.AreEqual(4, suite.TestCount, "Not all tests were found."); } } }
What’s really interesting here is the TestAssemblyBuilder uses a singleton within the NUnit internals, CoreExtensions.Host, to resolve all the core components for building up the suite. This object implements the IExtensionHost interface which is also used to install our addin:
public interface NUnit.Core.Extensibility.IAddin { bool Install(IExtensionHost host); }
With this singleton at our disposal this is a pretty powerful option as I can now inject my addin into the framework with any configuration or mock that I want and test without environment dependencies. In this example, I’m able to test how many times my Selenium instance would be started without ever launching a java host or browser:
namespace SeleniumToolkit.NUnit.Tests { using NUnit.Core.Builders; using Selenium; using SeleniumToolkit; [TestFixture] public class FixtureBuilderTests { [Test] public void When_Fixture_Owns_Session_SeleniumFactory_Should_Only_Create_Sessions_For_Fixtures() { // create config that influences how the addin will work inside NUnit SeleniumConfiguration config = new SeleniumConfiguration(); config.AddinSettings.RecyclePerFixture = true; // create basic event listener and suite-builder for installation into addin var eventListener = new SeleniumEventListener(null); // null = no java host var suiteBuilder = new SeleniumTestFixtureBuilder(config); // mock out the factory that creates our selenium objects var factory = MockRepository.GenerateMock<ISeleniumFactoryProvider>(); var selenium = MockRepository.GenerateMock<ISelenium>(); // tell our mock'd factory to always return our mock selenium factory.Expect(f => f.Create(null,0,null,null)).IgnoreArguments() .Return(selenium).Repeat.Any(); // set our expectations for how many times selenium will start/stop selenium.Expect(s => s.Start()).Repeat.Times(3); selenium.Expect(s => s.Stop()).Repeat.Times(3); // inject our mock into the toolkit SeleniumFactory.Configure(config, factory); // install addin SeleniumNUnitAddin addin = new SeleniumNUnitAddin(); addin.Install(CoreExtensions.Host, suiteBuilder, eventListener); // injection method // using installed addin, test construction of fixtures var assemblyBuilder = new TestAssemblyBuilder(); TestSuite suite = assemblyBuilder.Build( "NUnit.TestAssembly.FixtureScenario.dll", false); // run our sample fixtures against our configured runtime suite.Run(eventListener); // verify our behavior is as expected selenium.VerifyAllExpectations(); } } }
I should point out here that I don’t have my addin installed in NUnit’s addin folder. In fact, because I’m manually injecting it into NUnit, installation isn’t necessary. I also discovered that having the addin installed introduced some unexpected behavior with the tests in a few places.
To ensure that I'm testing my addin in isolation, it's important to clean up the CoreExtensions singleton after each test. While the NUnit core provides a mechanism to remove items from its extension points, it requires that you pass in the instances you want to remove, which didn’t quite fit my needs. I suppose I could have implemented equality comparisons in my extensions, but I opted for the hack ‘em out using reflection option. It’s gross, but it works:
public class AddinUtil { public static void Uninstall() { IExtensionPoint eventListeners = CoreExtensions.Host.GetExtensionPoint("EventListeners"); IExtensionPoint suiteBuilders = CoreExtensions.Host.GetExtensionPoint("SuiteBuilders"); IExtensionPoint testBuilders = CoreExtensions.Host.GetExtensionPoint("TestCaseBuilders"); RemoveExtension(typeof(SeleniumEventListener), eventListeners); RemoveExtension(typeof(SeleniumTestFixtureBuilder), suiteBuilders); RemoveExtension(typeof(SeleniumTestCaseBuilder), testBuilders); } private static void RemoveExtension(Type type, IExtensionPoint extensionPoint) { ArrayList items = GetInternalExtensions(extensionPoint); ArrayList toRemove = new ArrayList(); foreach (object item in items) { if (type.IsAssignableFrom(item.GetType())) { toRemove.Add(item); } } foreach (object item in toRemove) { items.Remove(item); } } private static ArrayList GetInternalExtensions(IExtensionPoint extensionPoint) { object extensions = extensionPoint.GetType() .GetField("extensions", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) .GetValue(extensionPoint); return (ArrayList)extensions; } }
Conclusion
This technique provides a brief glimpse of how to use automate testing nunit addins inside nunit using dependency injection. I’m fairly certain that I will still have to manually verify the addin’s behaviour from time to time, but with this approach I hopefully won’t have to do it that often.
No comments:
Post a Comment