"Вызывающий поток должен быть STA, потому что это требуется во многих компонентах пользовательского интерфейса" во время модульного тестирования.
У меня есть класс с именем OptionsWindow, который наследует от Window, который предназначен для выбора из опций в окне. И Диалог Класс, который имеет дело с этими диалогами. В моем тесте я пытаюсь высмеять выбор, выбранный в диалоге.
[TestMethod]
public async Task Test()
{
dialog.Setup(e => e.ShowDialog(It.IsAny<Window>(), It.IsAny<IntPtr>()))
.Returns(true)
.Callback<Window, IntPtr>((w, ip) => {
if (w.DataContext != null && w.DataContext is OptionsViewModel ovm)
ovm.Result = -1;
});
await tester.ShowWindow();
//assert....
}
Затем в тестируемом классе у меня есть эти методы.
public async Task ShowWindow()
{
var res = ShowDialog();
//do other stuff...
}
private int ShowDialog()
{
OptionsViewModel vm = //.....
dialog.ShowDialog(new OptionsWindow(vm));
return vm.Result;
}
однако я получаю сообщение об ошибке "Вызывающий поток должен быть STA, потому что это требуется для многих компонентов пользовательского интерфейса", когда он пытается установить Result of OptionsViewModel.
Во время ручного тестирования все работало нормально и не было проблем с многопоточностью, поэтому я не уверен, почему я получаю их здесь... любая помощь великолепна. Спасибо
(я использую Microsoft.VisualStudio.TestTools.UnitTesting кстати)
2 ответа
В моем тесте я пытаюсь высмеять выбор, выбранный в диалоге.
Обычно, если написание тестов затруднительно, это означает, что код должен быть лучше разработан.
В этом случае прямая зависимость от компонентов пользовательского интерфейса не является идеальной. Здесь может помочь шаблон, называемый портами и адаптерами (он же гексагональная архитектура, он же чистая архитектура). Таким образом, вы определяете интерфейсы с точки зрения приложения, а затем небольшие объекты-адаптеры реализуют эти интерфейсы.
Таким образом, вы можете сделать так, чтобы приложение определяло интерфейс, который обеспечивает то, что ему нужно
public interface IUserInteraction
{
int ModalOptionsWindow();
}
с реализацией:
public sealed class WpfUserInteraction : IUserInteraction
{
int ModalOptionsWindow()
{
OptionsViewModel vm = //.....
dialog.ShowDialog(new OptionsWindow(vm));
return vm.Result;
}
}
Что именно покрывает интерфейс, зависит от вас. Обычно я предпочитаю хранить мои ViewModels на стороне порта приложения и иметь только виды на стороне интерфейса UI.
Когда у вас есть интерфейс, введите IUserInteraction
и ваш код называет это. После этого модульное тестирование упрощается.
Однако, если вы находитесь в унаследованном сценарии с кодом, где вам нужно написать тесты перед рефакторингом, вы можете выполнить модульный тест кода пользовательского интерфейса. Это просто не легко. Увидеть WpfContext
или же WindowsFormsContext
в этом старом архиве для способа создания потока STA и прокачки сообщений из модульного теста.
Мой вариант использования был похожим, и мне помогло создать поток STA внутри метода тестирования. Таким образом, я смог использовать пользовательский интерфейс Winforms, который иначе не запустился бы.
[TestMethod("JUST THE BASIC: Microsoft.VisualStudio.TestTools.UnitTesting")]
public async Task TestMethod1()
{
// Use a semaphore to prevent the [TestMethod] from returning prematurely.
SemaphoreSlim ss = new SemaphoreSlim(1);
await ss.WaitAsync();
Thread thread = new Thread(() =>
{
// Verify
Assert.IsTrue(Thread.CurrentThread.GetApartmentState() == ApartmentState.STA);
// Log a message to the Unit Test
Console.WriteLine($"Thread State is {Thread.CurrentThread.GetApartmentState()}.");
// I personally needed to test a Winforms UI and
// the DragDrop COM wouldn't register without STA.
var myUI = new System.Windows.Forms.Form();
myUI.HandleCreated += (sender, e) =>
{
AutomateMyUI(myUI);
};
System.Windows.Forms.Application.Run(myUI);
// Signal that the [TestMethod] can return now.
ss.Release();
});
// Just make sure to set the apartment state BEFORE starting the thread:
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
await ss.WaitAsync();
Console.WriteLine("All done!");
}
Затем, чтобы проверить концепцию, я сделал короткую процедуру, которая циклически переключает цвета.
/// <summary>
/// DEMO: Sweeps though some UI colors before closing UI.
/// </summary>
private async void AutomateMyUI(System.Windows.Forms.Form myUI)
{
await Task.Delay(1000);
myUI.BackColor = Color.LightSalmon;
await Task.Delay(1000);
myUI.BackColor = Color.LightGreen;
await Task.Delay(1000);
myUI.BackColor = Color.LightYellow;
await Task.Delay(1000);
myUI.BackColor = Color.LightBlue;
myUI.Close();
}