开发者

How to programmatically start a WPF application from a unit test?

Problem

VS2010 and TFS2010 support creating so-called Coded UI Tests. All the demos I have found, start with the WPF application already running in the background when the Coded UI Test begins or the EXE is started using the absolute path to it.

I, however, would like to start my WPF application under test from the unit test code. That way it'll also work on the build server and on my peer's working copies.

How do I accomplish that?

My discoveries so far

a) This post shows how to start a XAML window. But that's not what I want. I want to start the App.xaml because it contains XAML resources and there is application logic in the code behind fi开发者_JS百科le.

b) The second screenshot on this post shows a line starting with

ApplicationUnterTest calculatorWindow = ApplicationUnderTest.Launch(...);

which is conceptually pretty much what I am looking for, except that again this example uses an absolute path the the executable file.

c) A Google search for "Programmatically start WPF" didn't help either.


MyProject.App myApp = new MyProject.App();
myApp.InitializeComponent();
myApp.Run();


I am doing something similar in VS2008 and manually creating the tests using UI Spy to help me identify the controls and some helper methods, not shown, to trigger button clicks and verify values on the screen. I use the Process object to launch the application I am testing in the TestInitialize method and in the TestCleanup method I close the process. I have a number of ways to ensure the process has completely closed in the CleanUp. As for the absolute path problem, I just programmatically lookup the current path and append my application's executable. Since I don't know how long it takes for the application to start I put an AutomationId in my main window and set it to "UserApplicationWindow" and wait for that to be visible, of course, you may have something else you could wait for. Finally, I use the MyTestClass as a base class and extend the class for different tests.

[TestClass]
public class MyTestClass
{
    private Process _userAppProcess;
    private AutomationElement _userApplicationElement ;

    /// <summary>
    /// Gets the current directory where the executables are located.  
    /// </summary>
    /// <returns>The current directory of the executables.</returns>
    private static String GetCurrentDirectory()
    {
        return Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase).AbsolutePath).Replace("%20", " ");
    }

    [TestInitialize]
    public void SetUp()
    {
        Thread appThread = new Thread(delegate()
        {
            _userAppProcess = new Process();
            _userAppProcess.StartInfo.FileName =GetCurrentDirectory() + "\\UserApplication.exe";
            _userAppProcess.StartInfo.WorkingDirectory = DirectoryUtils.GetCurrentDirectory();
            _userAppProcess.StartInfo.UseShellExecute = false;
            _userAppProcess.Start();
        });
        appThread.SetApartmentState(ApartmentState.STA);
        appThread.Start();

        WaitForApplication();
    }

    private void WaitForApplication()
    {
        AutomationElement aeDesktop = AutomationElement.RootElement;
        if (aeDesktop == null)
        {
            throw new Exception("Unable to get Desktop");
        }

        _userApplicationElement = null;
        do
        {
            _userApplicationElement = aeDesktop.FindFirst(TreeScope.Children,
                new PropertyCondition(AutomationElement.AutomationIdProperty, "UserApplicationWindow"));
            Thread.Sleep(200);
        } while ( (_userApplicationElement == null || _userApplicationElement.Current.IsOffscreen) );

    }

    [TestCleanup]
    public void CleanUp()
    {
        try
        {
            // Tell the application's main window to close.
            WindowPattern window = _userApplicationElement.GetCurrentPattern(WindowPattern.Pattern) as WindowPattern ;
            window.Close();
            if (!_userAppProcess.WaitForExit(3000))
            {
                // We waited 3 seconds for the User Application to close on its own.  
                // Send a close request again through the process class.
                _userAppProcess.CloseMainWindow();
            }

            // All done trying to close the window, terminate the process
            _userAppProcess.Close();
            _userAppProcess = null; 
        }
        catch (Exception ex)
        {
            // I know this is bad, but catching the world is better than letting it fail.
        }
    }
}


I ended up using ApplicationUnderTest.Launch(...) (MSDN) which is automatically created when recording an automated test with Microsoft Test Manager.


Here's what I just hacked together that succeeds in unit-testing caliburn micro a little bit:

[TestFixture]
public class when_running_bootstrapper
{
    [Test]
    public void it_should_request_its_view_model()
    {
        TestFactory.PerformRun(b =>
            CollectionAssert.Contains(b.Requested, typeof(SampleViewModel).FullName));
    }

    [Test]
    public void it_should_request_a_window_manager_on_dotnet()
    {
        TestFactory.PerformRun(b => 
            CollectionAssert.Contains(b.Requested, typeof(IWindowManager).FullName));
    }

    [Test]
    public void it_should_release_the_window_manager_once()
    {
        TestFactory.PerformRun(b =>
            Assert.That(b.ReleasesFor<IWindowManager>(), Is.EqualTo(1)));
    }

    [Test]
    public void it_should_release_the_root_view_model_once()
    {
        TestFactory.PerformRun(b =>
            Assert.That(b.ReleasesFor<SampleViewModel>(), Is.EqualTo(1)));
    }
}

static class TestFactory
{
    public static void PerformRun(Action<TestBootStrapper> testLogic)
    {
        var stackTrace = new StackTrace();
        var name = stackTrace.GetFrames().First(x => x.GetMethod().Name.StartsWith("it_should")).GetMethod().Name;
        var tmpDomain = AppDomain.CreateDomain(name,
            AppDomain.CurrentDomain.Evidence,
            AppDomain.CurrentDomain.BaseDirectory,
            AppDomain.CurrentDomain.RelativeSearchPath,
            AppDomain.CurrentDomain.ShadowCopyFiles);
        var proxy = (Wrapper)tmpDomain.CreateInstanceAndUnwrap(typeof (TestFactory).Assembly.FullName, typeof (Wrapper).FullName);

        try
        {
            testLogic(proxy.Bootstrapper);
        }
        finally
        {
            AppDomain.Unload(tmpDomain);
        }
    }
}

[Serializable]
public class Wrapper
    : MarshalByRefObject
{
    TestBootStrapper _bootstrapper;

    public Wrapper()
    {
        var t = new Thread(() =>
            {
                var app = new Application();
                _bootstrapper = new TestBootStrapper(app);
                app.Run();
            });
        t.SetApartmentState(ApartmentState.STA);
        t.Start();
        t.Join();
    }

    public TestBootStrapper Bootstrapper
    {
        get { return _bootstrapper; }
    }
}

[Serializable]
public class TestBootStrapper
    : Bootstrapper<SampleViewModel>
{
    [NonSerialized]
    readonly Application _application;

    [NonSerialized]
    readonly Dictionary<Type, object> _defaults = new Dictionary<Type, object>
        {
            { typeof(IWindowManager), new WindowManager() }
        };

    readonly Dictionary<string, uint> _releases = new Dictionary<string, uint>();
    readonly List<string> _requested = new List<string>();

    public TestBootStrapper(Application application)
    {
        _application = application;
    }

    protected override object GetInstance(Type service, string key)
    {
        _requested.Add(service.FullName);

        if (_defaults.ContainsKey(service))
            return _defaults[service];

        return new SampleViewModel();
    }

    protected override void ReleaseInstance(object instance)
    {
        var type = instance.GetType();
        var t = (type.GetInterfaces().FirstOrDefault() ?? type).FullName;

        if (!_releases.ContainsKey(t))
            _releases[t] = 1;
        else
            _releases[t] = _releases[t] + 1;
    }

    protected override IEnumerable<object> GetAllInstances(Type service)
    {
        throw new NotSupportedException("Not in this test");
    }

    protected override void BuildUp(object instance)
    {
        throw new NotSupportedException("Not in this test");
    }

    protected override void Configure()
    {
        base.Configure();
    }

    protected override void OnExit(object sender, EventArgs e)
    {
        base.OnExit(sender, e);
    }

    protected override void OnStartup(object sender, System.Windows.StartupEventArgs e)
    {
        base.OnStartup(sender, e);

        _application.Shutdown(0);
    }

    protected override IEnumerable<System.Reflection.Assembly> SelectAssemblies()
    {
        return new[] { typeof(TestBootStrapper).Assembly };
    }

    public IEnumerable<string> Requested
    {
        get { return _requested; }
    }

    public uint ReleasesFor<T>()
    {
        if (_releases.ContainsKey(typeof(T).FullName))
            return _releases[typeof (T).FullName];
        return 0u;
    }
}

[Serializable]
public class SampleViewModel
{
}


This may not be quite what you want, but I had a similar problem with my WPF apps and their coded UI tests. In my case I am using TFS build (via the Lab template) and its deploy takes the output of our build; an MSI and installs that on the target, the tests then run against the installed software.

Now because we are wanting to test against the installed software we added test initialise methods that start the GUI we test by calling the MSI API to get the install folder for the product/component GUIDs in our installer.

Here's a code extract, remember to substitute your product and component GUIDS from your installer)

    /// <summary>
    /// Starts the GUI.
    /// </summary>
    public void StartGui()
    {
        Console.WriteLine("Starting GUI process...");
        try
        {
            var path = this.DetectInstalledCopy();
            var workingDir = path;
            var exePath = Path.Combine(path, "gui.exe");

            //// or ApplicationUnderTest.Launch() ???
            Console.Write("Starting new GUI process... ");
            this.guiProcess = Process.Start(new ProcessStartInfo
            {
                WorkingDirectory = workingDir,
                FileName = exePath,
                LoadUserProfile = true,
                UseShellExecute = false
            });
            Console.WriteLine("started GUI process (id:{0})", this.guiProcess.Id);
        }
        catch (Win32Exception e)
        {
            this.guiProcess = null;
            Assert.Fail("Unable to start GUI process; exception {0}", e);
        }
    }

    /// <summary>
    /// Detects the installed copy.
    /// </summary>
    /// <returns>The folder in which the MSI installed the GUI feature of the cortex 7 product.</returns>
    private string DetectInstalledCopy()
    {
        Console.WriteLine("Looking for install directory of CORTEX 7 GUI app");
        int buffLen = 1024;
        var buff = new StringBuilder(buffLen);
        var ret = NativeMethods.MsiGetComponentPath(
            "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}",   // YOUR product GUID (see WiX installer)
            "{YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}",   // The GUI Installer component GUID
            buff,
            ref buffLen);

        if (ret == NativeMethods.InstallstateLocal)
        {
            var productInstallRoot = buff.ToString();
            Console.WriteLine("Found installation directory for GUI.exe feature at {0}", productInstallRoot);
            return productInstallRoot;
        }

        Assert.Fail("GUI product has not been installed on this PC, or not for this user if it was installed as a per-user product");
        return string.Empty;
    }

    /// <summary>
    /// Stops the GUI process. Initially by asking nicely, then chopping its head off if it takes too long to leave.
    /// </summary>
    public void StopGui()
    {
        if (this.guiProcess != null)
        {
            Console.Write("Closing GUI process (id:[{0}])... ", this.guiProcess.Id);
            if (!this.guiProcess.HasExited)
            {
                this.guiProcess.CloseMainWindow();
                if (!this.guiProcess.WaitForExit(30.SecondsAsMilliseconds()))
                {
                    Assert.Fail("Killing GUI process, it failed to close within 30 seconds of being asked to close");
                    this.guiProcess.Kill();
                }
                else
                {
                    Console.WriteLine("GUI process closed gracefully");
                }
            }

            this.guiProcess.Close();    // dispose of resources, were done with the object.
            this.guiProcess = null;
        }
    }

And here's the API wrapper code:

    /// <summary>
    /// Get the component path.
    /// </summary>
    /// <param name="product">The product GUI as string with {}.</param>
    /// <param name="component">The component GUI as string with {}.</param>
    /// <param name="pathBuf">The path buffer.</param>
    /// <param name="buff">The buffer to receive the path (use a <see cref="StringBuilder"/>).</param>
    /// <returns>A obscure Win32 API error code.</returns>
    [DllImport("MSI.DLL", CharSet = CharSet.Unicode)]
    internal static extern uint MsiGetComponentPath(
        string product,
        string component,
        StringBuilder pathBuf,
        ref int buff);
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜