Search code examples
multithreadingunit-testingjunit4maven-surefire-plugin

Is there a way to report lingering threads after every test class? (junit/maven)


In a badly written legacy codebase, the unit tests invoke code that fires up threads which are never stopped. In most cases it does not have an effect, but in some cases it slows down the build considerably, in others it causes completely unclear side-effects between tests in the same project during the build: e.g. test A starts thread, then B runs in the same JVM and breaks in some undefined manner (the fix is to have A stop that thread).

Is there some tool that can be used in conjunction with junit, so that at the end of every test (or set of tests in one class) it fails the test if there are any threads left running?

This would allow us to quickly identify and fix all existing cases, but also prevent new tests from being written this way.


Solution

  • 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>
    ...