如何在单元测试中以编程方式启动WPF应用程序?

12

问题

VS2010和TFS2010支持创建所谓的编码 UI 测试。我找到的所有演示都是在开始 Coded UI 测试时,WPF 应用程序已经在后台运行,或者使用绝对路径启动 EXE。

然而,我想从单元测试代码中启动我的 WPF 应用程序进行测试。这样它也可以在构建服务器上和我的同事的工作副本上运行。

我该如何实现?

我迄今为止的发现

a)此帖子展示了如何启动 XAML 窗口。但那不是我想要的。我想要启动 App.xaml,因为它包含 XAML 资源,并且代码后面有应用程序逻辑。

b)此帖子上的第二个截图显示了一行开头为

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

这个概念上基本符合我的需求,但是这个例子再次使用了可执行文件的绝对路径。

c)Google搜索“以编程方式启动WPF”也没有帮助。

5个回答

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

4
我正在使用VS2008,并手动使用UI Spy创建测试,以帮助我识别控件和一些辅助方法(未显示),以触发按钮点击并验证屏幕上的值。我使用进程对象在TestInitialize方法中启动我正在测试的应用程序,在TestCleanup方法中关闭该进程。我有多种方式来确保进程已完全关闭。至于绝对路径问题,我只需编程查找当前路径并附加我的应用程序可执行文件即可。由于我不知道应用程序启动需要多长时间,因此在我的主窗口中放置了一个AutomationId,并将其设置为“UserApplicationWindow”,等待其可见,当然,您可能有其他等待方式。最后,我使用MyTestClass作为基类,并扩展该类以进行不同的测试。
[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.
        }
    }
}

谢谢!我喜欢WaitForApplication()这部分。 - Lernkurve

1

我最终使用了ApplicationUnderTest.Launch(...) (MSDN),这是在使用Microsoft Test Manager录制自动化测试时自动生成的。


0

这是我刚刚拼凑起来的东西,能够在单元测试caliburn micro上取得一些成功:

[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
{
}

0

这可能不完全是你想要的,但我在我的WPF应用程序和它们的编码UI测试中遇到了类似的问题。在我的情况下,我正在使用TFS构建(通过实验室模板)和它的部署将我们的构建输出(一个MSI文件)安装到目标上,然后测试针对已安装的软件运行。

现在,因为我们希望针对已安装的软件进行测试,所以我们添加了测试初始化方法,通过调用MSI API来获取我们安装程序中产品/组件GUID的安装文件夹。

以下是代码片段,请记得从你的安装程序中替换你的产品和组件GUIDS)

    /// <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;
        }
    }

这里是API包装器代码:

    /// <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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接