Shutting down JUnit tests “gracefully” in eclipse

Motvation

I started working on an integration testing project recently. We’re using an integrated container (provided by cargo) to host the application under test and then have the selenium webdriver accessing the connecting to the server via browser.

In this environment, I bumped into a problem that I didn’t consider being one at first: Every time when clicking the terminate button during a test run in eclipse, it just kills the process – no @after or onTearDown() methods getting invoked nor is a shutdown hook called.
This leaves me with the problem that when the JVM that runs the test is killed, no cleanup is performed: The server isn’t stopped, the browser isn’t closed and the database isn’t cleaned up.
As a consequence, I have to do this manually after every test that I terminate, which, at least during active development, is about every single one, as these test unfortunately aren’t very performing. Doing so like 100 times a day really started to make me angry.

So I spent some time looking for a solution, which terminates JUnit tests within eclipse more gently, but no luck. A couple of guys even filled bugs at eclipse during the last years, from which not a single one ever got fixed.

I just wonder why eclipse just can’t call @after/onTearDown() when terminating JUnit tests?

Anyway, after getting inspired by a couple of stackoverflow posts, I put together my own rather simple “solution”, which is more of a workaround, really.
Still, I think it might be worth being posted here.

Solution overview

My approach includes having a separate thread listening to standard in, waiting for a specific “signal” – a string whose occurrence initiates the “soft” termination. Just like SIGINT on unix-like systems. Once the signal is received, the console listener calls System.exit() and leaves the cleanup to a shutdown hook.

The shutdown is realized as a user-defined Thread, which is registered as the JVM’s shutdown hook. This thread is executed by the runtime after System.exit() is invoked.

Speaking code

Sounds complex? Maybe a couple of lines of code illustrate the mechanism.

You can also find the following classes as a part of a demo-project from my github. So pull it and try it out for yourself!
The following class realizes the shutdown mechanisms mentioned above.

public class JUnitShutdown {
	/** Log4j logger. */
	private Logger log = Logger.getLogger(this.getClass());

	/** User defined shutdown hook thread. */
	private Thread shutdownHookThread;
	/**
	 * The "signal" string, whose input on the console initiates the shutdown of
	 * the test.
	 */
	private String exitSignalString;
	/** Listen for the signal only if the test is run in debug mode. */
	private boolean isDebugOnly;

	/**
	 * Creates an instance of the shutdownhook that listens to the console for
	 * an <code>exitSignal</code> and executes <code>shutdownHook</code> when
	 * the signal is received.
	 *
	 * @param exitSignal
	 *            the signal the leads to exiting the test.
	 * @param isUsedInDebugOnly
	 *            if <code>true</code>, <code>exitSignal</code> is only
	 *            evaluated if test is run in debug mode
	 * @param shutdownHook
	 *            the thread that is executed when <code>exitSignal</code> is
	 *            received.
	 */
	public JUnitShutdown(final String exitSignal,
			final boolean isUsedInDebugOnly, final Thread shutdownHook) {
		shutdownHookThread = shutdownHook;
		exitSignalString = exitSignal;
		this.isDebugOnly = isUsedInDebugOnly;
		initShutdownHook();
	}

	/**
	 * Allows for cleanup before test cancellation by listening to the console
	 * for a specific exitSignal. On this signal registers a shutdown hook who
	 * performs the cleanup in separate thread.
	 */
	private void initShutdownHook() {
		if (isDebugOnly
				&& java.lang.management.ManagementFactory.getRuntimeMXBean()
						.getInputArguments().toString()
						.contains("-agentlib:jdwp")) {
			return;
		}

		/* Start thread which listens to system.in */
		Thread consoleListener = new Thread() {
			@Override
			public void run() {
				BufferedReader bufferReader = null;
				try {
					bufferReader = new BufferedReader(new InputStreamReader(
							System.in));
					/* Read from system.in */
					while (!bufferReader.readLine().equals(exitSignalString)) {
						doNothing();
					}

					// Add shutdown hook that performs cleanup
					Runtime.getRuntime().addShutdownHook(shutdownHookThread);

					log.debug("Received exit signal \"" + exitSignalString
							+ "\". Shutting down test.");
					System.exit(0);
				} catch (IOException e) {
					log.debug("Error reading from console", e);
				}
			}

			/**
			 * Is not doing a thing.
			 */
			private void doNothing() {
			}
		};
		consoleListener.start();
	}

}

Example

Now what to do with JUnitShutdown?
The following rather senseless JUnit test keeps you machine busy for a while (like forever). At the beginning it opens an exemplary external resource (a file) open, which is closed and then deleted on cleanup. That is, entering “q” in your console during test execution results in closing and then deleting the resource, hitting the terminate button in eclipse will cause the file to remain on the system, however.

public class SomeLongRunningTest {
	/** Log4j logger. */
	private Logger log = Logger.getLogger(this.getClass());
	/**
	 * An exemplary resource which not deleted when the test gets terminated,
	 * but gets deleted, when using {@link JUnitShutdown}.
	 */
	private static final String SOME_FILE = "some.file";
	/**
	 * An exemplary resource which is not closed when the test gets terminated,
	 * but gets closed, when using {@link JUnitShutdown}.
	 */
	private BufferedWriter someResource = null;

	/** A value that controls how many values are output during the test. */
	private static final int SOME_DIVISOR = 100000000;

	/**
	 * The shutdown hook listening to the exit signal.
	 */
	@SuppressWarnings("unused")
	private JUnitShutdown shutdownHook = new JUnitShutdown("q", false,
			new Thread("testShutdownHook") {
				public void run() {
					cleanupResources("shutdown hook");
				}
			});

	/**
	 * Some exemplary test.
	 *
	 * @throws IOException
	 *             some error
	 */
	@Test
	public void testSomething() throws IOException {
		try {
			someResource = new BufferedWriter(new FileWriter(
					new File(SOME_FILE), false));

			doSomethingExpensive();

		} finally {
			cleanupResources("testSomething()");
		}
	}

	/**
	 * Keeps the machine busy forever.
	 *
	 * @throws IOException
	 *             something went wrong during writing to file
	 */
	private void doSomethingExpensive() throws IOException {
		int i = 0;
		while (true) {
			if (++i % SOME_DIVISOR == 0) {
				log.debug(i);
				someResource.write(i);
				someResource.newLine();
			}
		}
	}

	/**
	 * This method is only called when the test ends without being killed.
	 */
	@After
	public void onTearDown() {
		cleanupResources("onTearDown()");
	}

	/**
	 * Closes {@link #someResource} and deletes {@link #SOME_FILE}.
	 *
	 * @param caller
	 *            the caller of this method for logging purpose only.
	 */
	private void cleanupResources(final String caller) {
		log.debug("cleanupResources() called by " + caller);
		if (someResource != null) {
			try {
				someResource.close();
			} catch (IOException e) {
				log.error("Unable to close resource", e);
			}
		}
		File f = new File(SOME_FILE);
		if (f.exists() && !f.isDirectory()) {
			f.delete();
		}
	}
}

Running this test an pressing “q” and then pressing enter will produce a log output such as this:

2012-11-20 22:54:14,248 [main] DEBUG SomeLongRunningTest  info.schnatterer.test.sometest.SomeLongRunningTest.doSomethingExpensive(SomeLongRunningTest.java:78) 100000000
2012-11-20 22:54:15,260 [main] DEBUG SomeLongRunningTest  info.schnatterer.test.sometest.SomeLongRunningTest.doSomethingExpensive(SomeLongRunningTest.java:78) 200000000
q2012-11-20 22:54:16,175 [main] DEBUG SomeLongRunningTest  info.schnatterer.test.sometest.SomeLongRunningTest.doSomethingExpensive(SomeLongRunningTest.java:78) 300000000

2012-11-20 22:54:16,505 [Thread-0] DEBUG JUnitShutdown  info.schnatterer.test.shutdown.JUnitShutdown$1.run(JUnitShutdown.java:83) Received exit signal "q". Shutting down test.
2012-11-20 22:54:16,544 [testShutdownHook] DEBUG SomeLongRunningTest  info.schnatterer.test.sometest.SomeLongRunningTest.cleanupResources(SomeLongRunningTest.java:100) cleanupResources() called by shutdown hook

More advanced shutdowns

Where to go next?
I kept the examples above rather simple to make my point. Still, I’m sure that there is a lot you could extend or improve.
For instance, in the project mentioned above, I extended the mechanism, so now I have two signals:

  • “q” stops the server, closes the browser and deletes all the test data from the database.
  • “q!” (does that sound familiar?) is a bit faster, it omits the database-related stuff (as the data is set up at the beginning of each test run anyway).

I have to admit “q!” improves productivity tremendously 🙂

You may also have noted that this mechanism is not limited to be used with JUnit. I first considered implementing it using JUnit rules, then I found out that it would be easier and more generic not to do so.

Let me know if you have any ideas on how to further improve this mechanism.

Advertisement

2 thoughts on “Shutting down JUnit tests “gracefully” in eclipse

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.