Working with threads in test code¶
The standard unittest
infrastructure isn’t aware of threading. Therefore,
testing code that uses threads, or using threads in the test code itself, can
be somewhat inconvenient in a number of respects. The tl.testing.thread
module provides test-case and thread classes that help with the following:
- Collect errors and failures that occurred in threads other than the main one so they count with the test results.
- Silence expected unhandled exceptions in threads. Normally, such an exception would be printed in the middle of the test output, looking as if something was wrong.
- Report threads left behind by a test.
- Provide some convenience for starting a daemon thread. If a non-daemon thread doesn’t terminate, the test run will not complete: the process has to be killed.
- Provide some convenience for joining threads in test code. If a thread started by a test (or code under test) is still alive after a synchronisation point such as tear-down, it may interfere with subsequent tests. Also, some test runners may nag about threads left behind.
- Provide some convenience for counting threads started by test code or by the code under test. A simple count of threads alive won’t do since at least the main thread was already running before the test code was executed.
Let’s have a look at the convenience helpers first, then use them when explaining the more interesting stuff.
Synchronising with recently used threads¶
When making assertions about the set or the number of running threads (such as
when executing the examples further down in this documentation), there will be
synchronisation points in the main thread where one wants to join all threads
that should be finished at that point to make sure they are actually gone from
the accounting. The ThreadJoiner
class implements a context manager that
waits for any threads created by its code suite to be joined.
When instantiating a ThreadJoiner
, the time-out period must be configured:
>>> from tl.testing.thread import ThreadJoiner
>>> import threading
>>> import time
>>> with ThreadJoiner(0.2):
... thread = threading.Thread(target=lambda: time.sleep(0.1))
... thread.start()
>>> thread.is_alive()
False
By default, an error is raised if a thread that should be joined is still alive after the time-out:
>>> with ThreadJoiner(0.1):
... thread = threading.Thread(target=wait_forever)
... thread.start()
Traceback (most recent call last):
RuntimeError: Timeout joining thread <Thread(Thread-2, started 140700520466176)>
Clean up:
>>> kill_waiting_thread()
>>> thread.join(1)
>>> thread.is_alive()
False
The error may be suppressed if that makes more sense in the situation at hand. Let’s clean up this time by nesting two thread-joining context managers to contrast this notation against the explicit clean-up done for the previous example:
>>> with ThreadJoiner(1):
... with ThreadJoiner(0.1, check_alive=False):
... threading.Thread(target=wait_forever).start()
... kill_waiting_thread()
Moreover, the context manager provides information on those threads still alive after the attempt to join if the error was suppressed:
>>> with ThreadJoiner(1):
... with ThreadJoiner(0.1, check_alive=False) as joiner:
... threading.Thread(target=lambda: time.sleep(0.3)).start()
... threading.Thread(target=lambda: time.sleep(0.3)).start()
... threading.Thread(target=lambda: time.sleep(0)).start()
... joiner.left_behind
[<Thread(Thread-4, started 140423847343872)>,
<Thread(Thread-5, started 140423838951168)>]
Reporting threads left behind by a test¶
The stock unittest
framework doesn’t know about threading, therefore it
won’t report on threads left behind by tests. However, such threads may be a
problem that should be addressed. The ThreadAwareTestCase
class helps with
this by providing hooks for formatting and outputting a report on left-behind
threads. The default implementation prints the test in question and a list of
threads to standard output:
>>> from tl.testing.thread import ThreadAwareTestCase
>>> class SampleTest(ThreadAwareTestCase):
...
... def test_leave_behind_a_thread(self):
... threading.Thread(target=wait_forever).start()
...
... def test_dont_leave_behind_a_thread(self):
... pass
>>> import unittest
>>> with ThreadJoiner(1):
... run(unittest.makeSuite(SampleTest))
... kill_waiting_thread()
The following test left new threads behind:
__builtin__.SampleTest.test_leave_behind_a_thread
New thread(s):
<Thread(Thread-7, started 139691315910400)>
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
The message text of the report can be customised by overriding the
format_report_on_threads_left_behind
method:
>>> class SampleTest(ThreadAwareTestCase):
...
... def format_report_on_threads_left_behind(self, threads):
... return u'\nMessy test: %s\nThe mess: %r\n' % (self.id(), threads)
...
... def test_format_message_on_threads_left_behind(self):
... threading.Thread(target=wait_forever).start()
>>> with ThreadJoiner(1):
... run(unittest.makeSuite(SampleTest))
... kill_waiting_thread()
Messy test: __builtin__.SampleTest.test_format_message_on_threads_left_behind
The mess: [<Thread(Thread-7, started 139691315910400)>]
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Also, the method of reporting itself can be customised, for example, in order
to make it work with a graphical test runner. We’ll show how to have it log
the report in a list as a simple example. The test-case method to override is
report_threads_left_behind
:
>>> log = []
>>> class SampleTest(ThreadAwareTestCase):
...
... def report_threads_left_behind(self, threads):
... log.append(self.format_report_on_threads_left_behind(threads))
...
... def test_report_threads_left_behind(self):
... threading.Thread(target=wait_forever).start()
>>> with ThreadJoiner(1):
... run(unittest.makeSuite(SampleTest))
... kill_waiting_thread()
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
>>> print log[0]
The following test left new threads behind:
__builtin__.SampleTest.test_report_threads_left_behind
New thread(s):
<Thread(Thread-9, started 140060345734912)>
Starting a daemon thread¶
When starting a thread from a test, there’s normally no good reason for the test run to wait for that thread to terminate: whenever a piece of test code executed in the main thread has been run, no threads should remain from it as side effects and the test runner should be able to finish the test run. Therefore, test code should only start threads in daemon mode.
Since Python’s threading.Thread
class is built such that the daemon flag
has to be assigned after the thread has been instantiated, starting daemon
threads takes a few statements. The ThreadAwareTestCase
class provides a
convenience method that sets up and starts a daemon thread targeting a given
callable and returns the thread object:
>>> class SampleTest(ThreadAwareTestCase):
...
... def test_thread_should_be_started_as_daemon(self):
... thread = self.run_in_thread(wait_forever)
... self.assertTrue(isinstance(thread, threading.Thread))
If we don’t join the thread thus started during the test run, it will be left behind:
>>> with ThreadJoiner(1):
... run(unittest.makeSuite(SampleTest))
... kill_waiting_thread()
The following test left new threads behind:
__builtin__.SampleTest.test_thread_should_be_started_as_daemon
New thread(s):
<ExceptionReportingThread(Thread-1, started daemon 140060345734912)>
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Counting threads¶
Since threads may be left behind by some tests, we cannot assume that only the
main thread is active in the process when a test starts after another test
using threads has already been run. If we want to make assertions about the
number of threads started during the execution of a test, we therefore need to
leave out the threads that existed at the beginning of the current test run.
The ThreadAwareTestCase
provides a convenience method, active_count
to
take care of this mechanical detail:
>>> class SampleTest(ThreadAwareTestCase):
...
... def test_counting_threads_considers_only_those_started_by_test(self):
... with ThreadJoiner(1):
... self.run_in_thread(wait_forever)
... self.assertEqual(1, self.active_count())
... self.assertTrue(threading.active_count() > self.active_count())
... kill_waiting_thread()
>>> with ThreadJoiner(1):
... run(unittest.makeSuite(SampleTest))
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Threads started by the test’s set-up are also counted among the test’s own threads:
>>> class SampleTest(ThreadAwareTestCase):
...
... def setUp(self):
... self.thread = self.run_in_thread(wait_forever)
...
... def tearDown(self):
... kill_waiting_thread()
... self.thread.join(1)
...
... def test_counting_threads_considers_those_started_by_setup(self):
... self.assertEqual(1, self.active_count())
... self.assertTrue(threading.active_count() > self.active_count())
>>> with ThreadJoiner(1):
... run(unittest.makeSuite(SampleTest))
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
On the other hand, since the actual set of threads active at the test’s start is considered (as opposed to just the number of those threads), threads that finish while the test is running will not cause an accounting error:
>>> class SampleTest(ThreadAwareTestCase):
...
... def test_counting_threads_considers_only_those_started_by_test(self):
... with ThreadJoiner(1):
... self.run_in_thread(wait_forever)
... total_after_start = threading.active_count()
... kill_waiting_thread(i=1) # suppose a pre-existing thread
... existing_thread.join(1) # happens to finish just now
... self.assertTrue(threading.active_count() < total_after_start)
... self.assertEqual(1, self.active_count())
... kill_waiting_thread()
>>> with ThreadJoiner(1):
... existing_thread = threading.Thread(target=lambda: wait_forever(i=1))
... existing_thread.start()
... run(unittest.makeSuite(SampleTest))
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Collecting errors and failures from threads¶
Since the test runner isn’t aware of threads, it doesn’t control code execution in additional threads in the same way that it does in the main thread, meaning that it doesn’t collect failures nor errors for its reporting. Instead, failures and errors in threads other than the main one will be caught by the Python interpreter which simply prints a traceback at the point where the exception occurs.
In order to include errors and failures from all threads in the test report as
well as to suppress the noise of tracebacks printed by the interpreter,
tl.testing.thread
implements a thread class that intercepts exceptions
that would reach the interpreter and adds them to the current test’s result
object as errors or failures, respectively. The above-mentioned
run_in_thread
method of the ThreadAwareTestCase
already uses such a
thread by default:
>>> class SampleTest(ThreadAwareTestCase):
...
... def test_failed_assertions_in_thread_become_test_failures(self):
... with ThreadJoiner(1):
... self.run_in_thread(lambda: self.fail('just for fun'))
...
... def test_unhandled_exceptions_in_thread_become_test_errors(self):
... with ThreadJoiner(1):
... self.run_in_thread(lambda: 1/0)
>>> run(unittest.makeSuite(SampleTest))
======================================================================
ERROR: test_unhandled_exceptions_in_thread_become_test_errors (__builtin__.SampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
======================================================================
FAIL: test_failed_assertions_in_thread_become_test_failures (__builtin__.SampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: just for fun
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1, errors=1)
Silencing unhandled exceptions in threads¶
Sometimes, exceptions may occur in a thread that are of no immediate interest
to the developer. These exceptions would normally be printed among the
otherwise useful test output. To suppress such noise, the thread can be
started by using the run_in_thread
test-case method and passing a false
value for the report
option:
>>> class SampleTest(ThreadAwareTestCase):
...
... def test_unhandled_exceptions_in_thread_are_swallowed(self):
... with ThreadJoiner(1):
... self.run_in_thread(lambda: 1/0, report=False)
>>> run(unittest.makeSuite(SampleTest))
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK