Search code examples
c#c++unit-testingwinui-3c++-winrt

How to implement unit tests for WinUI3 (C++) application


At the moment, I have created a test project using a project template from Visual Studio Unit Test App (Winui 3) and my code looks like this

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
                );
            }
        }
    };
}

However, after starting the tests, I get the exception: "The Application Called An Interface that was Marshalled for a Different Thread". As far as I know, this exception thrown is due to the fact that I try to create an winrt::make< winrt::MyUI::implementation::BlankPage>() UI element outside the UI thread. I found a solution for C# which is to add an [UITestMethod] attribute for the test method. But how i can do unit tests like this in C++ test project?


Solution

  • The Problem is solved. As @Simon Mourier & @György Kőszeg suggested in comments, we can We can add a task to the ui thread queue to use the ui elements there. To access the ui thread queue, I used a global variable because I did not find any other way to access the ui thread queue other than through a global variable whose value is assigned in the OnLaunched method in the App class after activating the main window. Finally, solution looks like this:

    #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);
    }
    

    As you can see, I was also changed test framework to google test. If you want to do so, then you need:

    1. Install google test in your project
    2. Run google tests manually (you can do it in the App::OnLaunched method)

    Note: If you want to run tests which should enqueue callback in the UI thread, you should run tests in the separate thread, otherwise this action seems like produced deadlock.

        // 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);