Making fixtures available in setUpClass phase of TransactionTestCase

I encountered an asymmetry between TestCase and TransactionTestCase regarding when fixtures are made available. I’d like to propose eliminating the potential gotcha. [ticket] [pull request]

On databases that support transactions, TestCase loads fixtures only once, during setUpClass(). This is so that each test can roll back to the end of setUpClass()/setUpTestData().

TransactionTestCase flushes after each test instead of using transactions, so there’s less of a use case for loading fixtures on a class basis on the idea that you likely also want fixture data available during the tests themselves. I figure this is why TransactionTestCase doesn’t bother to override setUpClass().

However, I had cases where during class setup I wanted to call application logic that prepares temp directories and where this logic depended on fixtures (or more precisely, data from migrations, enabled with serialized_rollback=True, but the situation is the same as with the fixtures class attribute).

By trial and error, I found that the test data I was depending on weren’t available during setUpClass() but only during setUp(). I would have discovered this faster if I was using fixtures, but I happened to be using data from initial migrations with serialized_rollback, so the situation was more deceiving: my class with a single test method passed in isolation, since flushing only happens after each test, not before, and only failed in combination with other tests in the same pattern.

I think we can improve this situation:

  • The docs don’t caution that fixture data are only available in certain parts of the test lifecycle.
  • I think there’s a use case for depending on initial data in class setup even if TransactionTestCase will flush between tests, e.g. to perform side effects.
  • The serialized_rollback case is deceiving, causing difficult to debug failures when tests are combined after the fact.

Do folks agree this asymmetry is worth addressing?

Yes, I agree. Is it feasible to move the initial load up to setUpClass in TransactionTestCase ? I think it should be backwards-compatible enough. It could fail in setUpClass() methods that depend on data not existing, but that should be easy enough to adapt for.

Yeah, that’s essentially what I landed on in the PR. The load happens in _pre_setup(), so I lifted that up to setUpClass() and then memoized that it doesn’t need to happen before the first test’s setUp phase:

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        if not issubclass(cls, TestCase):
            cls._pre_setup()
            cls._test_pre_setup_ran_eagerly = True

This is more or less what TestCase does, except there it can just rely on checking whether the database supports transactions/savepoints instead of memoizing anything, and it runs _fixture_setup() eagerly instead of _pre_setup(), but TTC’s _pre_setup() is more involved, so I needed to lift the whole thing up instead.