Apparently Maven/Surefire allows you to hook a listener using configuration! This is a realistic way to vertically integrate the check by implementing it as part of org.junit.runner.notification.RunListener.
Here is what I am using now:
public class FailOnLingeringThreadsRunListener extends org.junit.runner.notification.RunListener
{
private Set<Thread> threadsBefore;
@Override
public synchronized void testRunStarted(Description description) throws Exception
{
threadsBefore = takePhoto();
super.testRunStarted(description);
}
@Override
public synchronized void testRunFinished(Result result) throws Exception
{
super.testRunFinished(result);
Set<Thread> threadsAfter = spotTheDiffs(threadsBefore);
// only complain on success, as failures may have caused cleanup code not to run...
if (result.wasSuccessful())
{
if (!threadsAfter.isEmpty())
throw new IllegalStateException("Lingering threads in test: " + threadsAfter);
}
}
public static Set<Thread> takePhoto()
{
return Collections.unmodifiableSet(Thread.getAllStackTraces().keySet());
}
@AfterClass
public static Set<Thread> spotTheDiffs(Set<Thread> threadsBefore)
{
Set<Thread> threadsAfter = Thread.getAllStackTraces().keySet();
if (threadsAfter.size() != threadsBefore.size())
{
threadsAfter.removeAll(threadsBefore);
return Collections.unmodifiableSet(threadsAfter);
}
return Collections.emptySet();
}
}
Here is how I enable it in the build:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<properties>
<property>
<name>listener</name>
<value>testutil.FailOnLingeringThreadsRunListener</value>
</property>
</properties>
</configuration>
</plugin>
...