【问题标题】:How to programmatically start a WPF application from a unit test?如何以编程方式从单元测试启动 WPF 应用程序?
【发布时间】:2011-01-23 15:02:27
【问题描述】:

问题

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

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

我该如何做到这一点?

到目前为止我的发现

a) 此帖子显示how to start a XAML window。但这不是我想要的。我想启动 App.xaml,因为它包含 XAML 资源,并且文件背后的代码中有应用程序逻辑。

b) this post 的第二个屏幕截图显示以

开头的一行
ApplicationUnterTest calculatorWindow = ApplicationUnderTest.Launch(...);

从概念上讲,这几乎就是我要寻找的,除了这个示例再次使用可执行文件的绝对路径。

c) Google search for "Programmatically start WPF" 也没有帮助。

【问题讨论】:

    标签: c# wpf unit-testing coded-ui-tests


    【解决方案1】:
    MyProject.App myApp = new MyProject.App();
    myApp.InitializeComponent();
    myApp.Run();
    

    【讨论】:

      【解决方案2】:

      我在 VS2008 中做类似的事情,并使用 UI Spy 手动创建测试,以帮助我识别控件和一些辅助方法(未显示),以触发按钮单击并验证屏幕上的值。我使用 Process 对象在 TestInitialize 方法中启动我正在测试的应用程序,并在 TestCleanup 方法中关闭进程。我有多种方法可以确保该过程在 CleanUp 中完全关闭。至于绝对路径问题,我只是以编程方式查找当前路径并附加我的应用程序的可执行文件。由于我不知道应用程序启动需要多长时间,所以我在主窗口中放置了一个 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() 部分。
      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        这是我刚刚拼凑起来的,成功地对 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
        {
        }
        

        【讨论】:

          【解决方案5】:

          这可能不是您想要的,但我的 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);
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-06-20
            • 1970-01-01
            • 1970-01-01
            • 2019-11-21
            相关资源
            最近更新 更多