nunit 测试中的模态对话框永远阻止测试运行程序
Modal Dialogs in nunit tests block test runner forever
我想对我的 UI class 进行单元测试(不实际显示)。我通过调用构造函数然后在其上调用各种方法来做到这一点(就像您调用普通的 class 一样)。 UI 绝不会被 windows 显示出来。然而,某些 UI 会在某些情况下抛出模态对话框,我想将其视为错误条件并使测试失败。
我已经尝试过超时属性,但它不起作用(测试 1),它只显示对话框并挂起测试。我有一个可行的实现 (Test2),但它有点难看。
是否有更简洁的方法将模态 windows 视为错误条件? / 即使显示模式对话也强制超时。
我正在使用 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");
}
}
}
更新
感谢您指点编码 UI 测试。这看起来是一个很好的潜在解决方案。
由于同时我确实得到了一些工作,所以我想我会更新它。此解决方案涉及 运行 具有自定义 timeout/shutdown 实现的 STA 线程中的测试。它是一个 NUnitAttribute,因此可以像 [Test] 一样使用。它很老套而且(大概)windows 具体但它似乎确实适合我的环境(我实际上根本不希望 UI 显示)。
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);
}
NUnit 真的没有能力测试 UI。假设您在 c# 中使用 Winforms 的标志,您应该为此切换到 CodedUI 测试。
尝试将代码从 类 形式中取出并绑定到视图模型也是可取的。视图和视图模型将是可测试的,您的代码将更清晰。
我想对我的 UI class 进行单元测试(不实际显示)。我通过调用构造函数然后在其上调用各种方法来做到这一点(就像您调用普通的 class 一样)。 UI 绝不会被 windows 显示出来。然而,某些 UI 会在某些情况下抛出模态对话框,我想将其视为错误条件并使测试失败。
我已经尝试过超时属性,但它不起作用(测试 1),它只显示对话框并挂起测试。我有一个可行的实现 (Test2),但它有点难看。
是否有更简洁的方法将模态 windows 视为错误条件? / 即使显示模式对话也强制超时。
我正在使用 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");
}
}
}
更新
感谢您指点编码 UI 测试。这看起来是一个很好的潜在解决方案。
由于同时我确实得到了一些工作,所以我想我会更新它。此解决方案涉及 运行 具有自定义 timeout/shutdown 实现的 STA 线程中的测试。它是一个 NUnitAttribute,因此可以像 [Test] 一样使用。它很老套而且(大概)windows 具体但它似乎确实适合我的环境(我实际上根本不希望 UI 显示)。
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);
}
NUnit 真的没有能力测试 UI。假设您在 c# 中使用 Winforms 的标志,您应该为此切换到 CodedUI 测试。 尝试将代码从 类 形式中取出并绑定到视图模型也是可取的。视图和视图模型将是可测试的,您的代码将更清晰。