Как реализовать модульные тесты для приложения WinUI3 (C++)

На данный момент я создал тестовый проект, используя шаблон проекта из приложения Unit Test App Visual Studio (Winui 3), и мой код выглядит следующим образом.

namespace TestMyWinrtApp
{
    TEST_CLASS(CppUnitTests)
    {
    public:
        TEST_METHOD(CppTestOne)
        {
            try {
                auto bp = winrt::make< winrt::MyUI::implementation::BlankPage>();
                Button btn = bp.FindName(L"myButton").as<winrt::Microsoft::UI::Xaml::Controls::Button>();
                winrt::hstring btnTag = btn.Tag().as<winrt::hstring>();
                Assert::IsTrue(btnTag == L"MyTag");
            } catch (const winrt::hresult_error& ex) {
                MessageBox(
                    NULL,                  
                    ex.message().c_str(), 
                    L"Error", 
                    MB_OK | MB_ICONINFORMATION
                );
            }
        }
    };
}

Однако после запуска тестов я получаю исключение: «Приложение, вызванное интерфейсом, который был настроен для другого потока». Насколько мне известно, это исключение связано с тем, что я пытаюсь создать элемент пользовательского интерфейса winrt::make< winrt::MyUI::implementation::BlankPage>() вне потока пользовательского интерфейса. Я нашел решение для C#: добавить атрибут [UITestMethod] для тестового метода. Но как я могу выполнять подобные модульные тесты в тестовом проекте C++?

Должен ли тест быть написан на c++-cli ​​?

πάντα ῥεῖ 09.06.2024 15:52

@πάνταῥεῖ нет, я хочу использовать стандарт C++17

Joe J 09.06.2024 15:56

Что ж, тогда код C# может быть сложно адаптировать для этого.

πάντα ῥεῖ 09.06.2024 15:57

@πάνταῥεῖ вообще, меня интересует любое решение на C++

Joe J 09.06.2024 15:58

Вероятно, вам лучше поискать c++-cli, поскольку он может (должен) поддерживать это [UITestMethod] так же легко и просто, как и с C#.

πάντα ῥεῖ 09.06.2024 16:29

@πάνταῥεῖ Я понятия не имею, как мне создать на 100% совместимое (с моим приложением WinUI3 C++) приложение C++/CLI

Joe J 09.06.2024 16:41

Либо пишите модульные тесты на C# для рабочего кода C++, либо используйте среду модульного тестирования C++, такую ​​как Google Test, Boost Test, Catch2 или (мой любимый) doctest. Кроме того, фреймворки модульного тестирования не так уж и сложно создать самостоятельно с нуля, имея лишь достаточную реализацию, чтобы удовлетворить потребности вашего проекта.

Eljay 09.06.2024 16:49

Если проблема в том, что вы используете компоненты WinUI (я вижу Button в вашем фрагменте), которым требуется работающий поток пользовательского интерфейса, то решением может быть создание обычного приложения WinUI и выполнение тестов вручную из метода OnLaunched. В связанном примере используется C# (и NUnit), но суть вы поняли. Записи Console перенаправляются в пользовательский элемент управления ConsoleRenderer, поэтому результат аналогичен выводу dotnet test ....

György Kőszeg 09.06.2024 16:51

Кроме того, из тестовых методов выполнение должно быть передано в поток пользовательского интерфейса с помощью DispatcherQueue.TryEnqueue. Опять же пример C# . И вот, как это можно использовать из тестового примера (обратите внимание на тело, завернутое в ExecuteTest(() => {...})).

György Kőszeg 09.06.2024 17:07

@GyörgyKőszeg Раньше у меня была похожая идея, но мне нужно стандартное решение. Если других предложений не будет, то, возможно, сделаю это. В любом случае, спасибо :)

Joe J 09.06.2024 17:07

То, что делает реализация .NET, не является волшебством, код находится здесь github.com/microsoft/testfx/blob/… вы можете сделать то же самое на C++, но это потребует немного работы, поскольку у вас нет класса Task (и связанный с ним) объект.

Simon Mourier 09.06.2024 17:55

@SimonMourier Проблема в том, что в модульном тесте я не могу получить доступ к потоку пользовательского интерфейса для добавления обратного вызова в очередь, потому что этот вызов winrt::Windows::ApplicationModel::Core::CoreApplication::Mai‌​nView().CoreWindow()‌ ​.Dispatcher().TryRun‌​Async(priority, callback) выдает исключение «не удалось создать новое представление, поскольку главное окно еще не создано», поэтому предложенный вами подход не работает в моем случае

Joe J 09.06.2024 18:51

В WinUI3 нет ни CoreApplication, ни CoreWindow, они предназначены только для UWP. Просто посмотрите на код: это пространство имен MicrosoftXXX, а не пространство имен WindowsXXX. Конечно, этот подход работает, это точно такая же среда, но вы просто используете разные классы.

Simon Mourier 09.06.2024 23:18

@SimonMourier, ты прав, я уже это делаю и хочу поблагодарить тебя (если тебя интересуют подробности, см. мой ответ ниже)

Joe J 11.06.2024 11:51
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
14
112
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема решена. Как предложили @Simon Mourier и @György Kőszeg в комментариях, мы можем добавить задачу в очередь потоков пользовательского интерфейса, чтобы использовать там элементы пользовательского интерфейса. Для доступа к очереди потоков пользовательского интерфейса я использовал глобальную переменную, поскольку не нашел другого способа доступа к очереди потоков пользовательского интерфейса, кроме как через глобальную переменную, значение которой присваивается в методе OnLaunched в классе App после активации главного окна. Наконец, решение выглядит так:

#include <winrt/base.h>
#include <winrt/Microsoft.UI.Xaml.Controls.h>
using namespace winrt::Microsoft::UI::Xaml::Controls;
extern winrt::Microsoft::UI::Dispatching::DispatcherQueue UIDispatcherQueue;

TEST(UITEST, first)
{
    std::promise<bool> p;
    std::future<bool> f = p.get_future();
    bool enqueued = UIDispatcherQueue.TryEnqueue(winrt::Microsoft::UI::Dispatching::DispatcherQueuePriority::High, [&p]()
    {
        auto bp = winrt::make< winrt::WinUIGtest::implementation::BlankPage>();
        Button btn = bp.FindName(L"myButton").as<Button>();
        winrt::hstring btnTag = btn.Tag().as<winrt::hstring>();
        bool isEquals = (btnTag == L"MyTag");
        p.set_value(isEquals);
    });

    bool res = f.get();
    ASSERT_EQ(res, true);
}

Как видите, я также изменил тестовую среду на Google Test. Если вы хотите это сделать, то вам нужно:

  1. Установите Google Test в свой проект
  2. Запустите тесты Google вручную (вы можете сделать это в методе App::OnLaunched)

Примечание. Если вы хотите запускать тесты, которые должны ставить обратный вызов в очередь в потоке пользовательского интерфейса, вам следует запускать тесты в отдельном потоке, иначе это действие будет выглядеть как вызванная взаимоблокировка.

    // allocate console to see gtest output
    if (AllocConsole()) {
        FILE* pStdout, * pStderr;
        freopen_s(&pStdout, "CONOUT$", "w", stdout);
        freopen_s(&pStderr, "CONOUT$", "w", stderr);
    }
    
    // run gtest in separate thread
    std::thread([]
    {
        int argc = 1;
        char** fakeArgv = new char* [1];
        char* path = new char[1024];
        strcpy_s(path, 1024, R"(C:\Users\UserName\source\repos\WinUIGtest\x64\Debug\WinUIGtest\WinUIGtest.exe)");
        fakeArgv[0] = path;
        ::testing::InitGoogleTest(&argc, fakeArgv);
        //::testing::InitGoogleMock(&argc, fakeArgv);
    
        RUN_ALL_TESTS();
    }).detach();
    
    // disable standard tests
    //winrt::Microsoft::VisualStudio::TestPlatform::TestExecutor::WinRTCore::UnitTestClient::Run(m_args);

Другие вопросы по теме