Testing Swing Application
I had to recently add UI tests for an application implemented with Swing library for the Posmulten project. The GUI does not do any rocket science. It does what the Posmulten project was created for, generating DDL statements that make RLS policy for the Postgres database, but with a user interface based on Swing components. Now, because the posmulten is an open-source project and the CI/CD process uses GitHub action, it would be worth having tests covering the UI application's functionality. Tests that could be run in a headless environment.
Testing Framework
As for testing purposes, I picked the AssertJ Swing library. It is effortless to mimic application users' actions. Not to mention that I could, with no effort, check application states and their components.
Below is an example of a simple test case that checks if the correct panel will show up with the expected content after entering text and clicking the correct button.
@Test
public void shouldDisplayCreationScriptsForCorrectConfigurationWhenClickingSubmitButton() throws SharedSchemaContextBuilderException, InvalidConfigurationException {
// GIVEN
String yaml = "Some yaml";
ISharedSchemaContext context = mock(ISharedSchemaContext.class);
Mockito.when(factory.build(eq(yaml), any(DefaultDecoratorContext.class))).thenReturn(context);
List<SQLDefinition> definitions = asList(sqlDef("DEF 1", null), sqlDef("ALTER DEFINIT and Function", null));
Mockito.when(context.getSqlDefinitions()).thenReturn(definitions);
window.textBox(CONFIGURATION_TEXTFIELD_NAME).enterText(yaml);
// WHEN
window.button("submitBtn").click();
// THEN
window.textBox(CREATION_SCRIPTS_TEXTFIELD_NAME).requireText("DEF 1" + "\n" + "ALTER DEFINIT and Function");
// Error panel should not be visible
findJTabbedPaneFixtureByName(ERROR_TAB_PANEL_NAME).requireNotVisible();
}
You can find the complete test code here.
Posmulten
The library for which the GUI application was created is generally a simple DDL statement builder that makes RSL policy in the Postgres database. The generated RLS policies allow applications communicating with the Postgres database to work in Mutli-tenant architecture with the shared schema strategy.
For more info, please check below links:
- Posmulten
- GUI module
- Shared Schema Strategy With Postgres
- Multi-tenancy Architecture With Shared Schema Strategy in Webapp Application Based on Spring-boot, Thymeleaf, and Posmulten-hibernate
Maven Configuration
It is worth excluding UI tests from unit tests. Although tests might not be fully e2e with mocked components, it is worth excluding from running together with unit tests because their execution might take a little longer than running standard unit tests.
<profile>
<id>swing-tests</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<configuration>
<includes>
<include>**/*SwingTest.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
Full Maven file.
To run tests locally, on an environment with a Graphics card, you need to execute tests with a maven wrapper, like below.
./mvnw -pl :openwebstart '-DxvfbRunningTests=true' -P !unit-tests,swing-tests test
GitHub Action
Now, moving to the GitHub action, running the UI test on the environment with a Graphics card seems easy. However, there might be situations when some UI windows with a WhatsApp or MS Teams notification appear on the Desktop on which UI tests are executed, and our tests will fail. Tests should be repeated in such cases, but that is not the problem.
Many more problems can occur when we try to execute tests on a headless environment, which is probably the default environment for every CI/CD pipeline. And we still need to run those tests and ensure they will pass no matter if they are executed in such an environment.
When we ask how to execute UI tests in a headless environment, the first suggestion on the internet is to use the Xvfb. However, the contributors of AssertJ Swing suggest a different approach.
Our tests maximize windows and do other stuff the default window manager of
xvfb
doesn't support. TightVNC makes it easy to use another window manager. Just addgnome-wm &
(or the window manager of your choice) to~/.vnc/xstartup
and you're ready to run.
GitHub
So, I followed suggestions from the contributors' team and used the Tightvncserver. I had some problems with adding the gnome-wm. Instead, I used the Openbox.
Below, you can see the step that runs UI tests.
The full GitHub action file can be found here.
The script files used to configure CI can be found here.
testing_swing_app:
needs: [compilation_and_unit_tests, database_tests, testing_configuration_jar]
runs-on: ubuntu-latest
name: "Testing Swing Application"
steps:
- name: Git checkout
uses: actions/checkout@v2
# Install JDKs and maven toolchain
- uses: actions/setup-java@v3
name: Set up JDK 11
id: setupJava11
with:
distribution: 'zulu' # See 'Supported distributions' for available options
java-version: '11'
- name: Set up JDK 1.8
id: setupJava8
uses: actions/setup-java@v1
with:
java-version: 1.8
- uses: cactuslab/maven-toolchains-xml-action@v1
with:
toolchains: |
[
{"jdkVersion": "8", "jdkHome": "${{steps.setupJava8.outputs.path}}"},
{"jdkVersion": "11", "jdkHome": "${{steps.setupJava11.outputs.path}}"}
]
- name: Install tightvncserver
run: sudo apt-get update && sudo apt install tightvncserver
- name: Install openbox
run: sudo apt install openbox
- name: Copy xstartup
run: mkdir $HOME/.vnc && cp ./swing/xstartup $HOME/.vnc/xstartup && chmod +x $HOME/.vnc/xstartup
- name: Setting password for tightvncserver
run: ./swing/setpassword.sh
- name: Run Swing tests
id: swingTest1
continue-on-error: true
run: ./mvnw -DskipTests --quiet clean install && ./swing/execute-on-vnc.sh ./mvnw -pl :openwebstart '-DxvfbRunningTests=true' -P !unit-tests,swing-tests test
#https://www.thisdot.co/blog/how-to-retry-failed-steps-in-github-action-workflows/
# https://stackoverflow.com/questions/54443705/change-default-screen-resolution-on-headless-ubuntu
- name: Run Swing tests (Second time)
id: swingTest2
if: steps.swingTest1.outcome == 'failure'
run: ./mvnw -DskipTests --quiet clean install && ./swing/execute-on-vnc.sh ./mvnw -pl :openwebstart '-DxvfbRunningTests=true' -P !unit-tests,swing-tests test
Retry Failed Steps in GitHub Action Workflows
As you probably saw in the GitHub action file, the last step, which executes the UI tests, is added twice. After correctly setting up Tightvncserver and Openbox, I didn't observe that the second step had to be executed, except at the beginning during deployment when there were not a lot of UI components. I used Xvfb, and sometimes, the CI passed only after the second step. So even if there is no problem with executing the test the first time right now, then it is still worth executing those tests the second time in case of failure. To check if the first step failed, we first have to name it. In this case, the name is "swingTest1". In the second step, we use the "if" property like the below:
if: steps.swingTest1.outcome == 'failure'
And that is generally all for running the step a second time in case of failure.
Check this resource if you want to check other ways to execute the step a second time.
Summary
Setting CI for UI tests might not be a trivial task, but it can benefit a project with any GUI. Not all things can be tested with unit tests.