I’m automating some tests that are currently run manually in a batch script that spins up the same executable multiple times, with varying options. As always, I’d like to get the tests started without modifying existing code, and preferably without adding any new code.
Unfortunately the application poses a few problems. The entry class has a curiously-implemented singleton behaviour to ensure that only one is running in a process. It also keeps a static ref to a set of command line options, but doesn’t give the client any way of clearing those options to restart processing. I also need to inject some behaviour that would allow the test to sense that the correct thing had been done. The class looks a bit like this:
namespace AppDomainInNUnit { internal class BaseApplication { public static BaseApplication Instance = null; public BaseApplication() { if(Instance == null) { Instance = this; } else { throw new InvalidOperationException("ctor fail"); } } class Application : BaseApplication { static int Main(string[] args) { var app = new Application(); return app.Run(); } int Run() { _options.Add("option", "added"); foreach(var option in _options.Keys) Console.WriteLine("{0} {1}", option, _options[option]); return 0; } public static Action<string> PostFinalStatus = s => Console.WriteLine(s); private static readonly Dictionary<string, string> _options = new Dictionary<string, string>(); } }
I could add methods to the existing code to clear the options and to handle the singleton behaviour, but that means changes. In principle there’s no need, as I can instantiate the classes I need in a separate AppDomain, and unload that AppDomain (and dump any static state) when I’m done.
To get this under test I need to do several things. First, I need to create a facade for the Application class that can be marshalled across AppDomain boundaries. Second, I need to consistently create and unload AppDomains in NUnit tests – I want to unload at the end of each test to dump the class’ static state. Finally, I want to inject some behaviour for the PostFinalStatus delegate that will allow the test to sense the state posted by the class.
So, first I need a facade for Application. This is partly because I need to create a class that can be marshalled across AppDomain boundaries. To use marshal-by-reference semantics, and have the instance execute in the new AppDomain, it’ll need to inherit MarshalByRefObject. Unfortunately I need to break the no-changes constraint, and add an internal hook to allow the facade to kick the Application class off.
The TestMain bootstrap class is a basically a copy of the existing Main( … ):
internal static int TestMain(string[] args) { return Main(args); }
Then I have a simple Facade:
public class Facade : MarshalByRefObject { public Facade() { Console.WriteLine("Facade default ctor in {0}", Thread.GetDomain().FriendlyName); } public void Run(string[] args) { Console.WriteLine("Calling to {0}.", Thread.GetDomain().FriendlyName); Application.TestMain(args); } }
Creating an AppDomain in an NUnit test is fairly straightforward, although it confused me for a while until I realised that the tests are run under a different application – in my case either the JetBrains TestRunner, or nunit-console. In either case the code is not actually available under the executable’s application base directory. The solution is to explicitly set the application base directory for the new test AppDomain:
[Test] public void Test() { var callingDomain = Thread.GetDomain(); var callingDomainName = callingDomain.FriendlyName; Console.WriteLine("{0}\n{1}\n{2}", callingDomainName, callingDomain.SetupInformation.ApplicationBase, callingDomain.SetupInformation.PrivateBinPath); var domain = AppDomain.CreateDomain("test-domain", null, null); Console.WriteLine("{0}\n{1}\n{2}", domain.FriendlyName, domain.SetupInformation.ApplicationBase, domain.SetupInformation.PrivateBinPath); AppDomain.Unload(domain); var setup = new AppDomainSetup() { ApplicationBase = callingDomain.SetupInformation.ApplicationBase }; domain = AppDomain.CreateDomain("test-domain", null, setup); Console.WriteLine("{0}\n{1}\n{2}", domain.FriendlyName, domain.SetupInformation.ApplicationBase, domain.SetupInformation.PrivateBinPath); var assembly = Assembly.GetExecutingAssembly().CodeBase; var facade = domain.CreateInstanceFromAndUnwrap( assembly, "AppDomainInNUnit.Facade") as Facade; facade.Run(new [] { "some", "args" }); AppDomain.Unload(domain); }
There’s a lot of cruft in there, but the intent is to show which AppDomain the class is actually being instantiated in, for clarity.
Finally, I want my tests to be informed of the Application’s final status when it posts it. This is tricky; I’ve been able to do it by passing in a delegate that sets some property data on the test AppDomain, then querying the AppDomain property data during the test assert stage. First the facade needs a method that allows me to set the post behaviour for Application:
public void SetPostBehaviour(Action<string> behaviour) { Application.PostFinalStatus = behaviour; }
Then I need to pass a delegate with the right behaviour in during the test, and assert on the value of that data before unloading the test AppDomain:
... var assembly = Assembly.GetExecutingAssembly().CodeBase; var facade = domain.CreateInstanceFromAndUnwrap( assembly, "AppDomainInNUnit.Facade") as Facade; var posted = false; facade.SetPostBehaviour(s => Thread.GetDomain().SetData("posted", true)); facade.Run(new [] { "some", "args" }); Assert.IsTrue((bool)domain.GetData("posted")); ...
So, it's possible to work around these static classes without too many intrusive changes to the existing code. Generally:
- To instantiate and execute a class in another AppDomain the class should derive from MarshalByRefObject (see Richter, CLR via C#). You’ll get a proxy to the instance in the new domain.
- Don’t tag with Serializable instead of deriving from MarshalByRefObject – you’ll get a copy of the instance in the other domain, and will still run in the original domain.
- You can, but you shouldn’t invoke static methods or fields on your class that derives from MarshalRefByObject. It won’t give you the effect you expect: all static calls are made in the calling domain, not the separate domain. To make calls on static methods you need to create a facade or adapter that can be marshalled across the domain boundary, and have that make the static calls for you.
- In the various CreateInstance… methods on AppDomain remember to use a fully qualified type name for the class you want to instantiate. This generally isn’t clear from examples like that in Richter, who dump all their classes in the global namespace (tsk).
- If you’re creating an AppDomain in an NUnit test, remember that you might need to set the ApplicationBase for the domain to the directory holding your test code.
- I wanted to get AppDomain.ExecuteAssembly(string, string[]) working, which would mean that I wouldn’t need a TestMain hook in the existing code. I’ve not managed to get it working yet, though. In any event, I’d still need a facade that I could marshal across the boundary in order to set the status post behaviour.
No comments:
Post a Comment