Как программно запустить приложение WPF из unit test?

Проблема

VS2010 и TFS2010 поддерживают создание так называемых кодированных пользовательских интерфейсов. Все демо, которые я нашел, начинаются с приложения WPF, уже запущенного в фоновом режиме при запуске теста кодированного пользовательского интерфейса или запуска EXE с использованием абсолютного пути к нему.

Однако я хотел бы запустить тестируемое WPF-приложение из кода unit test. Таким образом, он также будет работать на сервере сборки и на моих рабочих торгах.

Как это сделать?

Мои открытия пока

a) В этом сообщении показано как запустить окно XAML. Но это не то, что я хочу. Я хочу запустить App.xaml, потому что он содержит ресурсы XAML, и в коде позади файла есть логика приложения.

b) Второй снимок экрана на этот пост показывает строку, начинающуюся с

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

что концептуально в значительной степени то, что я ищу, за исключением того, что снова этот пример использует абсолютный путь к исполняемому файлу.

c) A Поиск Google для "Программного запуска WPF" тоже не помог.

Ответ 1

В итоге я использовал ApplicationUnderTest.Launch(...) (MSDN), который автоматически создается при записи автоматического теста с помощью Microsoft Test Manager.

Ответ 2

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

Ответ 3

Я делаю что-то подобное в 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 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.
        }
    }
}

Ответ 4

Вот то, что я только что взломал, что немного удалось выполнить в блочном тестировании калиберн микро:

[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 и их кодированными пользовательскими интерфейсами. В моем случае я использую сборку TFS (через шаблон Lab), и ее развертывание выводит результат нашей сборки; MSI и устанавливает это на целевом компьютере, тесты затем выполняются против установленного программного обеспечения.

Теперь, поскольку мы хотим протестировать программное обеспечение , установленное, мы добавили методы инициализации тестов, которые запускают тестируемый графический интерфейс, вызывая API MSI, чтобы получить папку установки для 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);