Debugging a failing automated GUI test can be difficult. It's
generally not an efficient debugging technique to sit staring at your
computer there watching the test manipulate the UI until it crashes.
(Although, it can be fun to see the reactions of co-workers as you sit
with your arms folded while your computer seems to be working on its
own. ;-)
I
had a problem recently in debugging a test for an eclipse plugin, but,
luckily a couple of co-workers were able to point me toward a solution.
The tests in question were written using SWTBot (http://www.eclipse.org/swtbot/).
SWTBot is a great open source testing framework that provides a layer
of abstraction through its API to facilitate automated SWT and eclipse
plugin test development. SWTBot's API makes it easy to write automated
tests that exercise the UI and provide pass/fail information through its
implementation of assertions.
My
test was failing, but it was not immediately obvious if the problem was
a bug in the software under test, a bug in (gasp!) the test code, or
maybe even a bug in the test framework. After staring at the code for a
while, I fell into the trap of running the test over and over, while I
watched the UI. After a few attempts it dawned on me that this approach was crazy. I might as well have been watching old movies on TV.
What I needed to do is use a debugger to stop the test's execution while I examined the UI.
The problem was that while I could have used a debugger in Eclipse, I
wanted to be able to run the test in the same unattended configuration
(running under maven from the CLI) that it would have to use when it was
run in our test framework. But, at the same time, I also wanted to be
able to manipulate the program and the UI and access its source code
through a debugger in eclipse.
The test had to be run unattended with maven - but, how could I do this and use the debugger?
The answer was to use remote debugging. Before looking at how this works, let's look at some background on Java debugging in general.
Java Debugging
The
place to begin is with the Java Platform Debugger Architecture
(JPDA, http://docs.oracle.com/javase/6/docs/technotes/guides/jpda/architecture.html)
The JPDA provides a multi-layer debugging architecture. There are (3) elements involved:
- The debugger
- The process being debugged (the "debuggee")
- The channel over which the debugger and debuggee communicate
Each element makes use of one of the Java APIs provided by the JPDA:
- The debugger uses the Java Debug Interface (JDI). The JDI defines a high level interface that can be used to program a debugger.
- The debuggee uses the Java VM Tool Interface (JVM TI). The JVM TI defines the debugging services, such as inspecting the state of a running application, that a JVM provides.
- The communications channel makes use of the Java Debug Wire Protocol (JDWP). The JDWP defines the communications protocol (requests, messages, etc.) between the debugger and debuggee.
I
referred to "remote" debugging a minute ago. This is the case even
though the test being debugged was completely running on a local system.
What happens is that to debug the test, an eclipse Remote Java Application Debug configuration is defined and configured to listen to the JVM over a specified port.
The
JDWP communications channel is the vehicle that get used to connect the
debuggee (which in this case is the test running under maven) to the
debugger (which in this case is the Eclipse debugger).
Creating and remote debugging an SWTBot project turns out to be a simple 4-step process:
Step 1 - Creating a New SWTBot Plugin Project, Converting it to a Maven Project
Creating
a new SWTBot plugin project in Eclipse is as easy as, well, installing
SWTBot into Eclipse and then creating a new project.
Let's
start by creating a new SWTBot project. Now, since we want to debug a
SWTBot test, we'll include buggy test class in the project. Here's the
code for our buggy little test:
package simple;
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(SWTBotJunit4ClassRunner.class)
public class BuggyTest {
@Test
public void canCreateANewJavaProject() throws Exception {
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(SWTBotJunit4ClassRunner.class)
public class BuggyTest {
@Test
public void canCreateANewJavaProject() throws Exception {
SWTWorkbenchBot bot;
bot = new SWTWorkbenchBot();
bot.viewByTitle("Welcome").close();
bot = new SWTWorkbenchBot();
bot.viewByTitle("Welcome").close();
bot.menu("File").menu("New").menu("Project...").click();
SWTBotShell shell = bot.shell("New Project");
shell.activate();
bot.tree().expandNode("Java").select("Java Project");
bot.button("Next >").click();
bot.textWithLabel("Project nname:").setText("TestProject");
// oops - it should be "name" not "nname"
bot.button("Finish").click();
}
}
bot.button("Finish").click();
}
}
Since
we want to be able to run the project with Maven outside of Eclipse,
the project then has to be converted to a Maven project. This conversion
is also simple from within Eclipse. Just right-click on the project
name, then select Configure->Convert to Maven Project
Before
we move on, we have to configure our Maven project to download both
SWTBot and to configure an Eclipse Test Platform to be used to run the
test. Luckily, this is easy to handle in the project's pom.xml file:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>simpleSWTBot</groupId>
<artifactId>simpleSWTBot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>eclipse-test-plugin</packaging>
<properties>
<tycho-version>0.11.0-SNAPSHOT</tycho-version>
</properties>
<repositories>
<repository>
<id>juno</id>
<layout>p2</layout>
<url>http://download.eclipse.org/releases/juno</url>
</repository>
<repository>
<id>swtbot</id>
<layout>p2</layout>
<url>http://download.eclipse.org/technology/swtbot/releases/2.1.0/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>sonatype</id>
<url>https://repository.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.tycho</groupId>
<artifactId>tycho-maven-plugin</artifactId>
<version>${tycho-version}</version>
<extensions>true</extensions>
</plugin>
<plugin>
<groupId>org.sonatype.tycho</groupId>
<artifactId>target-platform-configuration</artifactId>
<version>${tycho-version}</version>
<configuration>
<resolver>p2</resolver>
</configuration>
</plugin>
<plugin>
<groupId>org.sonatype.tycho</groupId>
<artifactId>maven-osgi-test-plugin</artifactId>
<version>${tycho-version}</version>
<configuration>
<useUIHarness>true</useUIHarness>
<useUIThread>false</useUIThread>
<product>org.eclipse.sdk.ide</product>
<application>org.eclipse.ui.ide.workbench</application>
<dependencies>
<dependency>
<type>p2-installable-unit</type>
<artifactId>org.eclipse.sdk.ide</artifactId>
<version>0.0.0</version>
</dependency>
</dependencies>
</configuration>
</plugin>
</plugins>
</build>
</project>
<modelVersion>4.0.0</modelVersion>
<groupId>simpleSWTBot</groupId>
<artifactId>simpleSWTBot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>eclipse-test-plugin</packaging>
<properties>
<tycho-version>0.11.0-SNAPSHOT</tycho-version>
</properties>
<repositories>
<repository>
<id>juno</id>
<layout>p2</layout>
<url>http://download.eclipse.org/releases/juno</url>
</repository>
<repository>
<id>swtbot</id>
<layout>p2</layout>
<url>http://download.eclipse.org/technology/swtbot/releases/2.1.0/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>sonatype</id>
<url>https://repository.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.tycho</groupId>
<artifactId>tycho-maven-plugin</artifactId>
<version>${tycho-version}</version>
<extensions>true</extensions>
</plugin>
<plugin>
<groupId>org.sonatype.tycho</groupId>
<artifactId>target-platform-configuration</artifactId>
<version>${tycho-version}</version>
<configuration>
<resolver>p2</resolver>
</configuration>
</plugin>
<plugin>
<groupId>org.sonatype.tycho</groupId>
<artifactId>maven-osgi-test-plugin</artifactId>
<version>${tycho-version}</version>
<configuration>
<useUIHarness>true</useUIHarness>
<useUIThread>false</useUIThread>
<product>org.eclipse.sdk.ide</product>
<application>org.eclipse.ui.ide.workbench</application>
<dependencies>
<dependency>
<type>p2-installable-unit</type>
<artifactId>org.eclipse.sdk.ide</artifactId>
<version>0.0.0</version>
</dependency>
</dependencies>
</configuration>
</plugin>
</plugins>
</build>
</project>
It's worthwhile to review a couple of elements in the pom.xml file as the file, while small, enables Maven to do quite a bit:
- The first repository definition enables Maven to download the version of Eclipse (Juno) artifacts that we'll use in the test, and the second repository enables Maven to download the SWTBot artifact that will be used. The Eclipse p2 provisioning system (http://www.eclipse.org/equinox/p2/) performs the downloads and installations.
- The Tycho plugins (http://www.sonatype.org/tycho) perform the actual building of the eclipse plugins that constitute the test and the extensions to Eclipse for SWTBot.
The
other project file that we have to edit is the MANIFEST.MF file. All we
have to do here is to ensure bundle version matches that defined in the
pom.xml file:
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: simpleSWTBot
Bundle-SymbolicName: simpleSWTBot;singleton:=true
Bundle-Version: 0.0.1.qualifier
Bundle-ActivationPolicy: lazy
Bundle-Vendor:
Bundle-RequiredExecutionEnvironment: J2SE-1.5
Require-Bundle: org.eclipse.swtbot.go
Bundle-ManifestVersion: 2
Bundle-Name: simpleSWTBot
Bundle-SymbolicName: simpleSWTBot;singleton:=true
Bundle-Version: 0.0.1.qualifier
Bundle-ActivationPolicy: lazy
Bundle-Vendor:
Bundle-RequiredExecutionEnvironment: J2SE-1.5
Require-Bundle: org.eclipse.swtbot.go
Now that our project is built and configured, the next step is to execute it with Maven outside of Eclipse.
Step 2 - Running the Test with Maven
Before
we try to debug the error that we've built into the test, let's run the
test so that our maven repo is fully populated with the necessary
Eclipse and SWTBot artifacts. We'll do this first as the first time we
run the test Maven's downloading these artifacts this may take several
minutes.
Configuring maven to avoid using mirror sites can make this run faster: -Dtycho.disableP2Mirrors=true
The command to install and run the test is: mvn clean install
When we run this program, we (eventually - after all the downloads are done) get this predictable error:
Could not find widget matching: (of type 'Text' and with label (with mnemonic 'Project nname:'))
Note
that you may also see problems using Java 1.7 - these may be caused by
the compression utility that Mavin is using to unpack jar files. If you
see errors such as:
[ERROR] Internal error: java.lang.IllegalArgumentException: Comparison method violates its general contract! -> [Help 1]
Then adding this setting to your command can resolve that problem:
-Djava.util.Arrays.useLegacyMergeSort=true
Now, we're all set to run the test with a remote debugger.
The best way to attach a remote debugger is to use the debugPort system property.
(See https://community.jboss.org/wiki/RemoteDebuggingForEclipseTestPlug-inRunningByTycho for details.)
(See https://community.jboss.org/wiki/RemoteDebuggingForEclipseTestPlug-inRunningByTycho for details.)
When re-run the test again, we see the following output:
mvn install -DdebugPort=8001
Listening for transport dt_socket at address: 8001
Listening for transport dt_socket at address: 8001
What's happening here is that the debugger is waiting for a remote program to connect to it on port 8001. Make
note of the port number (8001) as we'll need to reference that in the
Eclipse debug configuration that we'll use to run the test.
Step 3 - Add a Breakpoint to the Test Class
Now, back in Eclipse, let's add a breakpoint to the test class:
We're all set run the remote debugger now.
Step 4 - Creating and Running an Eclipse Debug Configuration
Next,
while still in Eclipse, select the test class that we want to execute,
select "Debug As," and create a new debug configuration. In the debug
configuration, specify that you want to run the test class as a Remote Java Application. Then, fill in the test project, the host is localhost and port 8001 and we're ready to go.
Starting the debugger makes the waiting test execution run and then stop on the first breakpoint. (Eclipse will also ask us if we want to switch to the Debug perspective.)
At this point, the SWTBot thread gets suspended and we can play with Eclipse UI. Lo and behold, there's the bug! We misspelled "name."
Summary
OK,
let's recap what we did here. We wanted to debug a failing SWTBot UI
test, and have the test run outside of Eclipse with Maven. We converted
the SWTBot project into a Maven project, reconfigured the project's
pom.xml file so Maven could download the Eclipse and SWTBot resources
that it needed to run, ran the test, and connected to it as a remote
Java application. And we did all this with only a few mouse clicks in
Eclipse!
(Special thanks to Michael Istria and Vlado Pakan for his help in writing this post!)
Information Sources