docs icon indicating copy to clipboard operation
docs copied to clipboard

Expand "Custom Testers" section

Open joelpop opened this issue 2 years ago • 2 comments

The Custom Testers section of the UI Unit Testing section could be expanded (possibly into its own page) and improved (including a discussion of best practices akin to the Tests with Page Objects page of the End-to-End Testing section).

The following example of how to implement calling the functions of a LitRenderer might be a good example to include as it is something that is a common need. A walkthrough of the code, explaining how it works—especially the getField method and "callables," would be extremely valuable.

Here is the example:

"Put this in a package, such as org.example.app.testers, with your other testers in test/main/java:"

public class GridLitRendererTester<T extends Grid<Y>, Y> extends GridTester<T, Y> {
    public GridLitRendererTester(T component) {
        super(component);
    }

    public void invokeLitRendererFunction(int row, String columnKey, String functionName) {
        var column = getColumn(columnKey);
        if (column.getRenderer() instanceof LitRenderer<Y> litRenderer) {
            var callable = findLitRendererFunction(litRenderer, functionName);
            callable.accept(getRow(row), Json.createArray());
        } else {
            throw new IllegalArgumentException("Target column doesn't have a LitRenderer.");
        }
    }

    private SerializableBiConsumer<Y, JsonArray> findLitRendererFunction(LitRenderer<Y> renderer, String functionName) {
        var callables = getField(LitRenderer.class, "clientCallables");
        try {
            var map = (Map<String, SerializableBiConsumer<Y, JsonArray>>) callables.get(renderer);
            var callable = map.get(functionName);
            if (callable == null) {
                throw new IllegalArgumentException("Function " + functionName + " not registered in LitRenderer.");
            }
            return callable;
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

"This is how a UI unit test can then use your new GridLitRendererTester:"

@ComponentTesterPackages("org.example.app.testers")  // <-- this matches the package where the above class is located
public class MyTest extends UIUnitTest {

    @Test
    void testMe() {
        GridView view = navigate(GridView.class);
        GridLitRendererTester<Grid<String>, String> gridPlus_ = test(GridLitRendererTester.class, view.grid);
        gridPlus_.invokeRendererFunction(1, "c2", "myFn");
        Assertions.assertEquals("two", test(view.clicked).getText());
        gridPlus_.invokeRendererFunction(0, "c2", "myFn");
        Assertions.assertEquals("one", test(view.clicked).getText());
    }

}

Thanks to @mcollovati for providing the initial code for the example!

(And alternatively, maybe for this particular tester, it should be integrated into the existing GridTester?)


If you think this issue is important, add a 👍 reaction to help the community and maintainers prioritize this issue.

joelpop avatar Dec 04 '23 14:12 joelpop

I’m still majorly struggling with creating Custom Testers. The section in our documentation presents everything in the reverse order of how it would normally be developed, thus making the context come after each snippet of code is presented. This makes the explanation impossible to follow on the first pass. Normally UI code is written first, you might then create a custom tester for it, then finally create a unit test using the custom tester to test your UI code. The documentation presents these in the reverse order. I would expect the documentation to walk me through the process in the natural order.

In addition, the provided unit test is empty. It does not show the actual use of the custom tester, though it does show the required annotation for the unit test to find the custom tester.

I would also like to see and better understand how to access the internal components of my custom component from my custom tester. I see there are getField and getMethod methods available in the custom tester. What do they do and how are they intended to be used?

joelpop avatar Dec 08 '23 05:12 joelpop

I have since learned a bunch about custom testers and find them to be wonderful to create for testing custom components, composites, dialogs, and views. They are akin to the end-to-end page-model-objects. So if I were to rewrite this chapter, I would start by presenting a simple view I want to test. Then I would present a Tester for it (using the find method to locate its components, store each of their testers in the Tester, and then provide methods that use those testers for interacting with the component like we do with our built in Testers). Then I would present the Test that uses the Tester (instead of embedding the component queries in the tests).

For an abbreviated example, this is a slimmed down version of what a custom tester could look like. For brevity, I have retained only three different field types and one button.

@Tests(PlanForm.class)
public class PlanFormTester<C extends PlanForm> extends ComponentTester<C> {

    private final TextFieldTester<TextField, String> $descriptionTextField;
    private final ComboBoxTester<ComboBox<PlanServer>, PlanServer> $planServerComboBox;
    private final CheckboxTester<Checkbox> $additionalDrawdownCheckbox;
    private final ButtonTester<Button> $saveButton;

    /**
     * Wrap PlanForm component for testing.
     *
     * @param planForm target PlanForm component
     */
    public PlanFormTester(C planForm) {
        super(planForm);
        ensureComponentIsUsable();

        $descriptionTextField = new TextFieldTester(find(TextField.class)
                .withCaption("Description")
                .single());
        $planServerComboBox = new ComboBoxTester(find(ComboBox.class)
                .withCaption("Server")
                .single());
        $aesCheckbox = new CheckboxTester(find(Checkbox.class)
                .withCaption("Enable AE's")
                .single());
        $saveButton = new ButtonTester<>(find(Button.class)
                .withCaption("Save")
                .single());
    }


    public String getDescription() {
        return $descriptionTextField.getComponent().getValue();
    }

    public void setDescription(String description) {
        $descriptionTextField.setValue(description);
    }

    public PlanServer getPlanServer() {
        return $planServerComboBox.getComponent().getValue();
    }

    public boolean isAes() {
        return Boolean.TRUE.equals($aesCheckbox.getComponent().getValue());
    }

    public void clickAes() {
        $aesCheckbox.click();
    }

    public void clickSave() {
        $saveButton.click();
    }
}

So now to manipulate the form as a user would, I simply do this in my test (GridPanelTester is another tester I wrote. Assertions omitted for clarity).

/**
 * Verify creating an item updates the grid rows, then delete it and verify the grid rows.
 */
@Test
@Transactional
void createItemSaveTest() {
    var $gridPanel = getGridPanelTester();
    $gridPanel.clickAddItem();

    var $planForm = test(PlanFormTester.class, $(PlanForm.class).single());

    $planForm.setDescription("createItemSaveTest plan");
    $planForm.clickEas();
    $planForm.clickSave();
    ...
}

joelpop avatar Jan 13 '24 00:01 joelpop