Модальные диалоги в nunit тестах навсегда блокируют тестирование - PullRequest
1 голос
/ 27 марта 2019

Я хочу провести модульное тестирование моих классов пользовательского интерфейса (фактически не показывая его). Я делаю это, вызывая конструктор, затем вызывая различные методы (как вы это делаете в обычном классе). Ни при каких условиях пользовательский интерфейс фактически не отображается окнами. Однако некоторые пользовательские интерфейсы при некоторых обстоятельствах выдают модальное диалоговое окно, я хочу трактовать это как условие ошибки и не выполнить тест.

Я пробовал атрибут Timeout, но он не работает (Test1), он просто показывает диалоговое окно и вешает тест. У меня есть реализация (Test2), которая работает, но это немного некрасиво.

Есть ли более чистый способ рассматривать модальные окна как условие ошибки? / форсировать таймаут, даже когда отображается модальный диалог.

Я использую тестовый прогон Visual Studio и nunit версии 3

class Test
{
    //does not work
    [Test, Timeout(5000)]
    public void Test1()
    {
        //blocks test and timeout is not respected
        MessageBox.Show("It went wrong");
    }

    //works but is ugly
    [Test]
    public void Test2()
    {
        Task runUIStuff = new Task(()=>
        {
            MessageBox.Show("It went wrong"); 

        });
        runUIStuff.Start();

        Task.WaitAny(Task.Delay(5000), runUIStuff);

        if(!runUIStuff.IsCompleted)
        {
            Process.GetCurrentProcess().CloseMainWindow();
            Assert.Fail("Test did not complete after timeout");
        }
    }
}

Обновление

Спасибо за указатель на кодированные тесты пользовательского интерфейса. Это выглядит как хорошее потенциальное решение.

Поскольку в то же время у меня что-то заработало, я решил обновить его. Это решение включает в себя запуск теста в потоке STA с пользовательской реализацией времени ожидания / завершения работы. Это атрибут NUnitAttribute, поэтому его можно использовать так же, как [Test]. Он довольно хакерский и (предположительно) специфичен для Windows, но, похоже, он действительно надежно работает для моей среды (где я вообще не хочу, чтобы пользовательский интерфейс вообще отображался).

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using NUnit.Framework.Internal.Commands;

namespace CatalogueLibraryTests.UserInterfaceTests
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    class UITimeoutAttribute : NUnitAttribute, IWrapTestMethod
    {
        private readonly int _timeout;

        /// <summary>
        /// Allows <paramref name="timeout"/> for the test to complete before calling <see cref="Process.CloseMainWindow"/> and failing the test
        /// </summary>
        /// <param name="timeout">timeout in milliseconds</param>
        public UITimeoutAttribute(int timeout)
        {
            this._timeout = timeout;
        }

        /// <inheritdoc/>
        public TestCommand Wrap(TestCommand command)
        {
            return new TimeoutCommand(command, this._timeout);
        }

        private class TimeoutCommand : DelegatingTestCommand
        {
            private int _timeout;

            public TimeoutCommand(TestCommand innerCommand, int timeout): base(innerCommand)
            {
                _timeout = timeout;
            }

            [DllImport("user32.dll", CharSet = CharSet.Auto)]
            private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, int wParam, IntPtr lParam);

            [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

            [DllImport("user32.dll")]
            static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);

            private string YesNoDialog = "#32770";

            private const UInt32 WM_CLOSE = 0x0010;
            private const UInt32 WM_COMMAND = 0x0111;
            private int IDNO = 7;


            public override TestResult Execute(TestExecutionContext context)
            {
                TestResult result = null;
                Exception threadException = null;

                Thread thread = new Thread(() =>
                {
                    try
                    {
                        result = innerCommand.Execute(context);
                    }
                    catch (Exception ex)
                    {
                        threadException = ex;
                    }
                });
                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();

                try
                {
                    while (thread.IsAlive && (_timeout > 0  || Debugger.IsAttached))
                    {
                        Task.Delay(100).Wait();
                        _timeout -= 100;
                    }

                    int closeAttempts = 10;

                    if (_timeout <= 0)
                    {
                        //Sends WM_Close which closes any form except a YES/NO dialog box because yay
                        Process.GetCurrentProcess().CloseMainWindow();

                        //if it still has a window handle then presumably needs further treatment
                        IntPtr handle;

                        while((handle = Process.GetCurrentProcess().MainWindowHandle) != IntPtr.Zero)
                        {
                            if(closeAttempts-- <=0)
                                throw new Exception("Failed to close all windows even after multiple attempts");

                            StringBuilder sbClass = new StringBuilder(100);

                            GetClassName(handle, sbClass, 100);

                            //Is it a yes/no dialog
                            if (sbClass.ToString() == YesNoDialog && GetDlgItem(handle, IDNO) != IntPtr.Zero)
                                //with a no button
                                SendMessage(handle, WM_COMMAND, IDNO, IntPtr.Zero); //click NO!
                            else
                                SendMessage(handle, WM_CLOSE, 0, IntPtr.Zero); //Send Close
                        }

                        throw new Exception("UI test did not complete after timeout");
                    }


                    if (threadException != null)
                        throw threadException;

                    if(result == null)
                        throw new Exception("UI test did not produce a result");

                    return result;
                }
                catch (AggregateException ae)
                {
                    throw ae.InnerException;
                }
            }
        }
    }
}

Использование

[Test, UITimeout(500)]
public void TestMessageBox()
{
    MessageBox.Show("hey");
}
[Test, UITimeout(500)]
public void TestMessageBoxYesNo()
{
    MessageBox.Show("hey","there",MessageBoxButtons.YesNo);
}

1 Ответ

2 голосов
/ 01 апреля 2019

NUnit действительно не оборудован для тестирования пользовательского интерфейса.Предполагая, что из флагов вы работаете с Winforms в c #, вы должны переключиться на тесты CodedUI для этого.Также рекомендуется попытаться извлечь код из классов форм и связать его с моделью представления.View и viewmodels будут тестируемыми, и ваш код будет чище.

...