Skip to content

Your first UI test#

This page shows how to set up a UI test suite and run a complete scenario. For the general test setup (aui_enable_tests, running Tests), see Writing tests. An overview of the module is at uitests.

How it works#

A UI test creates real windows and views, but no native window is opened. testing::UITest switches to AStubWindowManager and a software renderer, so the test runs on a machine without a display. The views are laid out, styled and rendered as usual, the screenshot of any moment is available, and input is delivered to the window the same way the platform would do it.

Include <AUI/UITest.h>; it brings By, all actions, all assertions and GTest.

Fixture#

Derive your suite from testing::UITest. Create and show the window in SetUp, and call the base class methods.

class UIClick : public testing::UITest {
public:

protected:
    class TestWindow : public AWindow {
    private:
        _<ALabel> mHelloLabel;

    public:
        TestWindow() {
            setContents(Centered {
              Vertical {
                _new<AButton>("Say hello")
                        .connect(&AView::clicked, this,
                                 [&] {
                                     mHelloLabel->setVisibility(Visibility::VISIBLE);
                                     onButtonClicked();
                                 })
                        .connect(&AView::clickedRightOrLongPressed, this, [&] { onButtonClickedRightOrLongPressed(); })
                        .connect(&AView::doubleClicked, this, [&] { onButtonDoubleClicked(); }) AUI_LET { it->setDefault(); },
                mHelloLabel = _new<ALabel>("Hello!") AUI_LET { it->setVisibility(Visibility::INVISIBLE); },
              },
            });

            pack();
        }

        MOCK_METHOD(void, onButtonClicked, (), ());
        MOCK_METHOD(void, onButtonDoubleClicked, (), ());
        MOCK_METHOD(void, onButtonClickedRightOrLongPressed, (), ());
    };

    void SetUp() override {
        UITest::SetUp();

        mTestWindow = _new<TestWindow>();
        mTestWindow->show();
    }

    void TearDown() override {
        mTestWindow = nullptr;
        UITest::TearDown();
    }

    _<TestWindow> mTestWindow;
};

The application under test here is a button "Say hello" that reveals a "Hello!" label. Note GMock: the window exposes MOCK_METHODs that the signal handlers call, so the test can tell whether the click was delivered.

Tip

Place the window in the fixture class as a nested class, as above, when it is only needed for tests. To test a window of your application, include its header and _new<MyWindow>()->show() it.

Test body#

Find a view, perform an action, check the outcome:

1
2
3
4
5
6
7
8
TEST_F(UIClick, HelloAppearsAfterClick) {
    EXPECT_CALL(*mTestWindow, onButtonClicked).Times(1);
    // press the button
    By::text("Say hello").perform(click());

    // check label is appeared
    By::text("Hello!").check(visible(), "label is not appeared");
}

By::text("Say hello") finds the button, click() clicks the center of it, and By::text("Hello!") finds the label and visible() checks that it is shown. See Finding views, Simulating user input and Checking views.

Typing and reading values#

Views can be found by their ASS name (<< "#username" in the view tree). The following window has a text field and a button; the test types a name, presses the button and checks the label.

class UIType: public testing::UITest {
public:
protected:
    void SetUp() override {
        UITest::SetUp();

        class TestWindow: public AWindow {
        private:
            _<ATextField> mTextField;
            _<ALabel> mHelloLabel;

        public:
            TestWindow() {
                setContents(Centered {
                        Vertical {
                                Horizontal {
                                        mTextField = _new<ATextField>() << "#username",
                                        _new<AButton>("Say hello").connect(&AView::clicked, this, [&] {
                                            if (mTextField->getText().empty()) {
                                                mHelloLabel->setVisibility(Visibility::INVISIBLE);
                                                return;
                                            }
                                            mHelloLabel->text() = "Hello, {}!"_format(mTextField->getText());
                                            mHelloLabel->setVisibility(Visibility::VISIBLE);
                                        }) AUI_LET { it->setDefault(); },
                                },
                                mHelloLabel = _new<ALabel>() AUI_LET {
                                    it->setVisibility(Visibility::INVISIBLE);
                                    it << "#hello";
                                }
                        }
                });

                pack();
            }
        };

        // prepare the window
        _new<TestWindow>()->show();
    }

    void TearDown() override {
        UITest::TearDown();
    }
};
TEST_F(UIType, HelloAppearsAfterClick) {
    // type "Steve" to the text field
    By::name("#username").perform(type("Steve"));

    // press the button
    By::text("Say hello").perform(click());

    // check label is appeared and contains text "Hello, Steve!"
    By::name("#hello").check(visible(), "label is not appeared")
                      .check(text("Hello, Steve!"), "invalid text");
}

Processing the UI#

A UI is not updated by itself in a test; nothing runs an event loop for you. Layout, style changes and queued messages (for example, lambdas posted with AUI_UI_THREAD) are processed when you call uitest::frame(). Every perform and check of UIMatcher calls it before and after the action, so most tests never need it. Call it explicitly when you changed something directly and want to look at the result:

label->setVisibility(Visibility::GONE);
uitest::frame();
EXPECT_EQ(otherView->getPosition().x, 0);

uitest::frame() also repacks every window (see AStubWindowManager::drawFrame), so a window is shrunk to the size of its contents unless the contents are expanding. Use AUI_REPEAT(10) { uitest::frame(); } to let a layout settle if it needs several passes.

Note

Timers and other asynchronous work do not advance on their own either. If your code waits for an AThread::sleep or a background task, call AThread::processMessages() after waiting, and prefer to make the code under test injectable (for instance, pass the delay as a parameter) so the test does not have to sleep.

Testing data binding#

Reactive UI is checked the same way: change the state and look at the views.

TEST_F(UIReactiveTest, Label) {
    // Test that a reactive label updates its text when the bound property changes.
    // The test creates a simple state object with a reactive string property
    // and binds it to a label using the AUI_REACT macro. It then verifies
    // the initial rendered text and the updated text after changing the
    // property.
    using namespace ass;

    struct State {
        AProperty<AString> name;
    };
    auto state = _new<State>();

    mWindow->setContents(Vertical {
        Label { AUI_REACT("{}!"_format(state->name)) }
    });

    // Initially, the property is empty, so the label should display "!".
    EXPECT_EQ(*_cast<ALabel>(By::type<ALabel>().one())->text(), "!");
    // Update the property; the label should automatically reflect the new value.
    state->name = "Hello";
    EXPECT_EQ(*_cast<ALabel>(By::type<ALabel>().one())->text(), "Hello!");
}

Cleaning up#

Release the windows and views you hold before calling UITest::TearDown(), as the fixtures above do (mTestWindow = nullptr;). UITest::TearDown() closes all windows and destroys the window manager.