12

问题

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

但是,我想从单元测试代码中启动我正在测试的 WPF 应用程序。这样它也可以在构建服务器和我对等的工作副本上工作。

我该如何做到这一点?

到目前为止我的发现

a) 这篇文章展示了如何启动 XAML 窗口。但这不是我想要的。我想启动 App.xaml,因为它包含 XAML 资源并且代码隐藏文件中有应用程序逻辑。

b)这篇文章的第二个屏幕截图显示了一行以

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

这在概念上几乎是我正在寻找的,除了这个例子再次使用可执行文件的绝对路径。

c) Google 搜索“以编程方式启动 WPF”也无济于事。

4

5 回答 5

5
MyProject.App myApp = new MyProject.App();
myApp.InitializeComponent();
myApp.Run();
于 2010-03-04T12:43:09.273 回答
4

我在 VS2008 中做类似的事情,并使用 UI Spy 手动创建测试来帮助我识别控件和一些辅助方法(未显示)来触发按钮单击并验证屏幕上的值。我使用 Process 对象在 TestInitialize 方法中启动我正在测试的应用程序,并在 TestCleanup 方法中关闭进程。我有多种方法可以确保该过程在 CleanUp 中完全关闭。至于绝对路径问题,我只是以编程方式查找当前路径并附加我的应用程序的可执行文件。由于我不知道应用程序启动需要多长时间,所以我在主窗口中放置了一个 AutomationId 并将其设置为“UserApplicationWindow”并等待它可见,当然,您可能还有其他可以等待的东西. 最后,

[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.
        }
    }
}
于 2010-03-08T23:49:25.360 回答
1

我最终使用了 ApplicationUnderTest.Launch(...) ( MSDN ),它是在使用 Microsoft 测试管理器记录自动化测试时自动创建的。

于 2010-03-14T14:33:05.590 回答
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
{
}
于 2012-05-08T20:08:59.633 回答
0

这可能不是您想要的,但我的 WPF 应用程序及其编码的 UI 测试也有类似的问题。就我而言,我使用的是 TFS 构建(通过 Lab 模板),它的部署获取我们构建的输出;MSI 并将其安装在目标上,然后针对已安装的软件运行测试。

现在因为我们想要针对已安装的软件进行测试,我们添加了测试初始化​​方法来启动我们测试的 GUI,方法是调用 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);
于 2013-07-16T20:11:56.923 回答