Skip to content

Testing Java Swing applications

Vladimir Turov edited this page Jan 28, 2021 · 5 revisions

In brief

In our Java testing library there is a possibility to test GUI applications based on Swing library. Testing of GUI applications written in Python is not yet supported. Testing is mainly based on the AssertJ library, which can simulate user behavior.

All examples are given from Text Editor project.

Swing tests

The process for testing Swing applications is as follows:

  1. Create an object (that extends JFrame) in the constructor of the test class.
  2. Define objects marked with @SwingComponent annotation.
  3. Create tests as methods marked with @DynamicTest annotation.
  4. Testing. The tests test the same object created in the constructor. So, it's important to note that the next test case will start with the object state where the previous test case ended. For example, if the 8th test opens a dialog box and checks that it has been opened and ends, the 9th test will start when the dialog box is already open. Or 4th test writes something in the text field then 5th test starts with this text field already being filled. This should be kept in mind.

Create an object

The object to be tested should be the main window of the application. For example, for the next student program:

package editor;

import javax.swing.*;

public class TextEditor extends JFrame {
    public TextEditor() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(300, 300);
        setVisible(true);
        setLayout(null);
    }
}

The constructor of the test class should look as follows:

import editor.TextEditor;
import org.hyperskill.hstest.stage.SwingTest;
import org.hyperskill.hstest.testcase.CheckResult;
import org.hyperskill.hstest.testcase.TestCase;

public class EditorTest extends SwingTest<Attach> {
    public EditorTest() {
        super(new TextEditor());
    }
}

You can access the FrameFixture object with the window variable and the original JFrame object with the frame variable. These variables are defined in the SwingTest class, so you do not need to create them yourself.

Swing components

There is an annotation called @SwingComponent. You can import it like the following:

import org.hyperskill.hstest.testing.swing.SwingComponent;

You can mark assertj-swing's fixtures with this annotation and hs-test library will search the appropriate component in the object's tree. The following components are supported:

  1. JButtonFixture
  2. JCheckBoxFixture
  3. JComboBoxFixture
  4. DialogFixture
  5. JFileChooserFixture
  6. JInternalFrameFixture
  7. JLabelFixture
  8. JListFixture
  9. JMenuItemFixture
  10. JPanelFixture
  11. JProgressBarFixture
  12. JRadioButtonFixture
  13. JScrollBarFixture
  14. JScrollPaneFixture
  15. JSliderFixture
  16. JSpinnerFixture
  17. JSplitPaneFixture
  18. JTabbedPaneFixture
  19. JTableFixture
  20. JTextComponentFixture
  21. JToggleButtonFixture
  22. JToolBarFixture
  23. JTreeFixture

You can import these components by the following path:

import org.assertj.swing.fixture.*;

All these fixtures represent the components without Fixture at the end that are located in the following path:

import javax.swing.*;

The library hs-test will find a component with an appropriate type and name that equals to the variable name capitalized by the first letter. For example, the following field:

@SwingComponent private JButtonFixture saveButton;

will represent the component with type JButton and name SaveButton. You can set the name to the component by using .setName(String) method. You do not need to initialize this field, the hs-test library will do it automatically before running the tests. If the library doesn't find such a component, it will show an error to the user with appropriate feedback. You can use these fixtures to access the components of the user program and do real tests. You can look at the AssertJ documentation to see what and how you can test in the user program.

The stage description should specify how to name certain components of the program so that they can be accessed from the tests. So, a stage description should contain something like this:

Due to testing reasons, you need to set name to some components.

Set the names to these components:

- JTextArea component to "TextArea"
- Field which contains filename to "FilenameField"
- Button that saves the file to "SaveButton"
- Button that loads a file to "LoadButton"
- ScrollPane to "ScrollPane"

You can customize the name of the component by providing name parameter to the annotation. Example:

@SwingComponent(name = "save") private JButtonFixture saveButton;

will represent the component with type JButton and name save.

Dynamic tests

Tests are methods defined with @DynamicTest annotation. You can see this page how to use such annotation.

One addition though: the library assertj-swing provides useful assertions like fixture.requireEmpty() or fixture.requireEnabled() and you can't provide feedback to them because if the check fails, the AssertionError will be thrown. The library will catch such an error and rethrow WrongAnswer without feedback, so you can use such assertions but no feedback isn't good. For this reason, you can use feedback parameter inside the @DynamicTest annotation.

Example:

@DynamicTest(feedback = "SaveButton should be enabled!")
CheckResult test1() {
    saveButton.requireEnabled();
    return correct();
}

In this example, a SaveButton should be enabled! feedback will be shown in case saveButton is not enabled.

There are some helper methods to check multiple components without such a hassle and within a single test:

  1. requireEnabled(Fixture...)
  2. requireDisabled(Fixture...)
  3. requireVisible(Fixture...)
  4. requireNotVisible(Fixture...)
  5. requireFocused(Fixture...)
  6. requireEditable(Fixture...)
  7. requireNotEditable(Fixture...)
  8. requireEmpty(Fixture...)

Example:

@DynamicTest
CheckResult test1() {
    requireEnabled(saveButton, loadButton, checkBox, textField);
    requireVisible(saveButton, loadButton, checkBox, textField);
    requireEmpty(textField);
    requireNotEditable(textField);
    return correct();
}

Full example

Here's full example of tests in the 3rd stage of the Text Editor project.

import editor.TextEditor;
import org.assertj.swing.fixture.JButtonFixture;
import org.assertj.swing.fixture.JMenuItemFixture;
import org.assertj.swing.fixture.JScrollPaneFixture;
import org.assertj.swing.fixture.JTextComponentFixture;
import org.hyperskill.hstest.dynamic.DynamicTest;
import org.hyperskill.hstest.stage.SwingTest;
import org.hyperskill.hstest.testcase.CheckResult;
import org.hyperskill.hstest.testing.swing.SwingComponent;
import org.junit.After;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static org.hyperskill.hstest.testcase.CheckResult.correct;

public class EditorTest extends SwingTest {
    public EditorTest() {
        super(new TextEditor());
    }

    @SwingComponent JTextComponentFixture textArea;
    @SwingComponent JTextComponentFixture filenameField;
    @SwingComponent JButtonFixture saveButton;
    @SwingComponent JButtonFixture loadButton;
    @SwingComponent JScrollPaneFixture scrollPane;
    @SwingComponent JMenuItemFixture menuFile;
    @SwingComponent JMenuItemFixture menuLoad;
    @SwingComponent JMenuItemFixture menuSave;
    @SwingComponent JMenuItemFixture menuExit;

    String filename1 = "SomeFile.txt";
    String filename2 = "AnotherFile.txt";
    String noExistFile = "FileDoesNotExist";

    String textToSave1 = "Basic text editor\nType here too\nHere also\n\n";
    String textToSave2 = "                Sonnet I\n" +
        "     \n" +
        "     \n" +
        "FROM fairest creatures we desire increase,\n" +
        "That thereby beauty's rose might never die,\n" +
        "But as the riper should by time decease,\n" +
        "His tender heir might bear his memory:\n" +
        "But thou, contracted to thine own bright eyes,\n" +
        "Feed'st thy light'st flame with self-substantial fuel,\n" +
        "Making a famine where abundance lies,\n" +
        "Thyself thy foe, to thy sweet self too cruel.\n" +
        "Thou that art now the world's fresh ornament\n" +
        "And only herald to the gaudy spring,\n" +
        "Within thine own bud buriest thy content\n" +
        "And, tender churl, makest waste in niggarding.\n" +
        "Pity the world, or else this glutton be,\n" +
        "To eat the world's due, by the grave and thee.\n" +
        "\n" +
        "                 Sonnet II                   \n" +
        "\n" +
        "\n" +
        "When forty winters shall beseige thy brow,\n" +
        "And dig deep trenches in thy beauty's field,\n" +
        "Thy youth's proud livery, so gazed on now,\n" +
        "Will be a tatter'd weed, of small worth held:\n" +
        "Then being ask'd where all thy beauty lies,\n" +
        "Where all the treasure of thy lusty days,\n" +
        "To say, within thine own deep-sunken eyes,\n" +
        "Were an all-eating shame and thriftless praise.\n" +
        "How much more praise deserved thy beauty's use,\n" +
        "If thou couldst answer 'This fair child of mine\n" +
        "Shall sum my count and make my old excuse,'\n" +
        "Proving his beauty by succession thine!\n" +
        "This were to be new made when thou art old,\n" +
        "And see thy blood warm when thou feel'st it cold.";

    @DynamicTest
    CheckResult test1() {
        requireEditable(textArea);
        requireEmpty(textArea, filenameField);
        requireEnabled(saveButton, loadButton);
        return correct();
    }

    @DynamicTest(feedback = "Can't enter multiline text in TextArea.")
    CheckResult test2() {
        textArea.setText(textToSave1);
        textArea.requireText(textToSave1);
        textArea.setText("");
        textArea.setText(textToSave2);
        textArea.requireText(textToSave2);
        return correct();
    }

    @DynamicTest(feedback = "Can enter multiline text in FilenameField, but shouldn't")
    CheckResult test3() {
        String text = textToSave1;
        filenameField.setText(text);
        filenameField.requireText(text.replace("\n", " "));
        filenameField.setText("");
        return correct();
    }

    @DynamicTest(feedback = "Text in FilenameField and in TextArea " +
        "should stay the same after saving file")
    CheckResult test4() {
        filenameField.setText(filename1);
        textArea.setText(textToSave1);

        saveButton.click();

        filenameField.requireText(filename1);
        textArea.requireText(textToSave1);
        return correct();
    }

    @DynamicTest(feedback = "Text in FilenameField and in TextArea " +
        "should stay the same after saving file")
    CheckResult test5() {
        String text = textToSave2;
        String file = filename2;

        filenameField.setText(file);
        textArea.setText(text);

        saveButton.click();

        filenameField.requireText(file);
        textArea.requireText(text);

        filenameField.setText("");
        textArea.setText("");
        return correct();
    }

    @DynamicTest(feedback = "Text in FilenameField stay the same after loading file")
    CheckResult test6() {
        String file = filename1;

        filenameField.setText(file);
        textArea.setText("");

        loadButton.click();

        filenameField.requireText(file);

        filenameField.setText("");
        textArea.setText("");
        return correct();
    }

    @DynamicTest(feedback = "Text should be the same after saving and loading same file")
    CheckResult test7() {
        String[] texts = {textToSave2, textToSave1};
        String[] files = {filename1, filename2};

        for (int i = 0; i < 2; i++) {

            String text = texts[i];
            String file = files[i];

            filenameField.setText("");
            textArea.setText("");

            filenameField.setText(file);
            textArea.setText(text);

            saveButton.click();

            filenameField.setText("");
            textArea.setText("");

            filenameField.setText(file);
            loadButton.click();

            textArea.requireText(text);
        }
        return correct();
    }

    @DynamicTest(feedback = "TextArea should be empty if user tries to " +
        "load file that doesn't exist")
    CheckResult test8() {
        textArea.setText(textToSave1);
        filenameField.setText(noExistFile);

        loadButton.click();
        textArea.requireText("");
        return correct();
    }

    @DynamicTest(feedback = "TextArea should correctly save and load an empty file")
    CheckResult test9() {
        textArea.setText("");
        filenameField.setText(filename1);

        saveButton.click();
        textArea.setText(textToSave2);
        loadButton.click();
        textArea.requireText("");
        return correct();
    }

    // menu-related tests

    @DynamicTest
    CheckResult test10() {
        requireEnabled(menuLoad, menuSave, menuFile, menuExit);
        return correct();
    }

    @DynamicTest(feedback = "Text in FilenameField and in TextArea " +
        "should stay the same after saving file using MenuSave")
    CheckResult test11() {
        filenameField.setText(filename1);
        textArea.setText(textToSave1);

        menuSave.click();

        filenameField.requireText(filename1);
        textArea.requireText(textToSave1);
        return correct();
    }

    @DynamicTest(feedback = "Text in FilenameField and in TextArea " +
        "should stay the same after saving file using MenuSave")
    CheckResult test12() {
        String text = textToSave2;
        String file = filename2;

        filenameField.setText(file);
        textArea.setText(text);

        menuSave.click();

        filenameField.requireText(file);
        textArea.requireText(text);

        filenameField.setText("");
        textArea.setText("");
        return correct();
    }

    @DynamicTest(feedback = "Text in FilenameField stay " +
        "the same after loading file using MenuLoad")
    CheckResult test13() {
        String file = filename1;

        filenameField.setText(file);
        textArea.setText("");

        menuLoad.click();

        filenameField.requireText(file);

        filenameField.setText("");
        textArea.setText("");
        return correct();
    }

    @DynamicTest(feedback = "Text should be the same after saving " +
        "and loading same file using MenuLoad")
    CheckResult test14() {
        String[] texts = {textToSave2, textToSave1};
        String[] files = {filename1, filename2};

        for (int i = 0; i < 2; i++) {

            String text = texts[i];
            String file = files[i];

            filenameField.setText("");
            textArea.setText("");

            filenameField.setText(file);
            textArea.setText(text);

            menuSave.click();

            filenameField.setText("");
            textArea.setText("");

            filenameField.setText(file);
            menuLoad.click();

            textArea.requireText(text);
        }
        return correct();
    }

    @DynamicTest(feedback = "TextArea should be empty if user tries to " +
        "load file that doesn't exist using MenuLoad")
    CheckResult test15() {
        textArea.setText(textToSave1);
        filenameField.setText(noExistFile);

        menuLoad.click();
        textArea.requireText("");
        return correct();
    }

    @DynamicTest(feedback = "TextArea should correctly save and load an empty file using menu")
    CheckResult test16() {
        textArea.setText("");
        filenameField.setText(filename1);

        menuSave.click();
        textArea.setText(textToSave2);
        menuLoad.click();
        textArea.requireText("");
        return correct();
    }

    @After
    public void deleteFiles() {
        try {
            Files.delete(Paths.get(filename1));
            Files.delete(Paths.get(filename2));
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}