skip to content

When is @unittest.skipIf's condition evaluated, and when must you call self.skipTest instead?

level: middleimportance: should knowfreq 30%

answer

  1. Decorator arguments are ordinary expressions
  2. It happens once, and early
  3. Before the runner, before setUp
  4. Live state needs a runtime call
  5. self.skipTest raises unittest.SkipTest

basics

~20 s

A skipIf condition is an ordinary expression evaluated once, when the decorator line executes at import time — before any test or its setup runs. Use self.skipTest(reason) inside setUp or the test body whenever the decision depends on state that only exists at run time.

solid answer

~40 s

`@unittest.skipIf(condition, reason)` is a plain function call: `condition` is evaluated when the class body is executed, i.e. when the module is imported, long before the runner starts. So it can read `sys.platform`, `os.environ` or a module-level capability probe, but it cannot see anything a fixture creates. When the decision needs live state — a service that turned out to be unreachable, a dataset that came back empty — call `self.skipTest(reason)` from inside `unittest.TestCase.setUp` or the test body; it raises `unittest.SkipTest` at that moment and yields the same skip outcome. The classic bug is passing a *function object* rather than calling it: `@unittest.skipIf(have_codec, ...)` is always truthy, so the test always skips and nobody notices.

code

python · 14 lines
python
import unittest


class ConversionQueueTests(unittest.TestCase):
    def setUp(self):
        self.queue_depth = 6800

    def test_batch_ordering(self):
        if self.queue_depth > 5000:
            self.skipTest(f"{self.queue_depth}-row batch is too large for the smoke run")
        self.assertEqual(self.queue_depth, 0)


unittest.main(argv=["prog"], verbosity=2, exit=False)

go deeper

for a junior

Remember there are two ways to skip: the decorators, which decide up front, and self.skipTest(reason), which decides while the test is running. Knowing that the second exists is the main thing at this level.

for a middle

Explain that a decorator argument is evaluated when the module is imported, so it cannot depend on fixture state, and that self.skipTest raises unittest.SkipTest at run time for the same reported outcome. Name the truthy-function-object trap.

for a senior

Show judgement about where the probe belongs: cheap, total, import-safe checks in decorators; anything that must be attempted against a live resource at run time. Be ready to explain why an import-time probe that raises loses the whole file.

for a principal

Own the convention across a large suite — one module-level capability constant rather than scattered probes, a rule that skip conditions never perform I/O at import, and how that keeps a multi-thousand-test suite's start-up cost bounded.

### A decorator argument is just an expression `@unittest.skipIf(condition, reason)` is not magic syntax; it is a call. Python evaluates `condition`, calls `unittest.skipIf` with the result, and applies the returned decorator to the function — all while executing the class body, which happens when the module is first imported. By the time `unittest`'s loader has built a suite, every `skipIf` and `skipUnless` decision is already frozen into the method. That has three practical consequences. **1. The condition cannot see fixture state.** `unittest.TestCase.setUp` runs per test, hundreds of milliseconds later, on a fresh instance. Anything it assigns is invisible to a decorator that was evaluated at import. Code that tries to reach it — a decorator referencing `self`, or a module-level flag the setup mutates — either fails outright or reads a stale value. **2. The condition runs even for tests that will not run.** Import happens before name filtering and before the runner chooses an order, so an expensive probe in a decorator argument costs time on every invocation of the suite, including one that selects a single unrelated test. Keep the probe cheap: a platform string, an environment variable, a module-availability check computed once at module level and reused by several decorators. **3. Import-time failure is not a skip.** If the probe itself raises — a connection attempt that times out, an attribute that does not exist — the module fails to import and the whole file is reported as an error, not a skip. Guard probes accordingly. ### The runtime route ```python def test_batch_conversion(self): if self.queue_depth > 5_000: self.skipTest("batch too large for the smoke run") ... ``` `unittest.TestCase.skipTest` raises `unittest.SkipTest`, which `unittest` catches and records as a skip with your reason — identical in the report to a decorator skip. You can raise `unittest.SkipTest` directly for the same effect; `skipTest` is the readable spelling. Where you call it matters: * **In the test body** — that one test is skipped. Anything `setUp` already did still gets torn down normally. * **In `setUp`** — the test method is skipped without running, and teardown behaves as it does for any aborted setup. * **At module import** (raising `unittest.SkipTest` at module level) — the whole module is reported skipped by discovery. ### Choosing between them Use the decorator when the answer is a property of the *environment the process started in*: the platform, the interpreter version, whether an optional capability was compiled in, a feature flag from `os.environ`. These are stable for the life of the process, cheap to evaluate, and worth stating declaratively where a reader sees them next to the test name. Use `self.skipTest` when the answer is a property of *this run's data or resources*: a dependency probe that must be attempted rather than inferred, a fixture that legitimately produced nothing to assert on, a case table row that does not apply to the configuration under test. ### The failure mode to name in an interview ```python def have_codec(): return ... @unittest.skipIf(have_codec, "codec missing") # WRONG: always truthy def test_x(self): ... ``` `skipIf` takes a **value**, not a predicate. A function object is always truthy, so this skips unconditionally, forever, silently — the suite stays green and the test never runs again. The fix is `@unittest.skipIf(not have_codec(), ...)`, or better, compute `HAS_CODEC = have_codec()` once at module level and decorate with that. The same trap catches truthy non-boolean values generally: a non-empty string reason accidentally passed as the condition behaves the same way. A related smell is the inverted predicate. `skipIf` skips when the condition is **true**; `skipUnless` skips when it is **false**. Reaching for `skipUnless(available, ...)` where "available" is the thing you need reads correctly and avoids the double negative that `skipIf(not available, ...)` introduces.

  • What happens if the expression you pass to @unittest.skipUnless raises?
    The exception propagates out of the class body during import, so the module fails to import. Discovery reports that as an error for the whole file, not as a skip — and none of the other tests in it run either. Probes in decorator arguments should therefore be total: catch the failure and turn it into a boolean.
  • Can you skip an entire module rather than a class?
    Yes — raise `unittest.SkipTest(reason)` at module level, outside any class. Discovery catches it and reports the module as skipped instead of erroring. It is the right tool when the file's whole subject is unavailable in this environment, such as a platform-specific extension the tests are written against.
  • Does calling self.skipTest halfway through a test undo what already ran?
    No. The test aborts at that line and is recorded as a skip, but any side effects already performed stand. Normal teardown and registered cleanups still run, so resources acquired before the skip are released — but a half-applied change to shared state is yours to avoid, which argues for skipping as early as possible, ideally in setup.

saying these in an interview costs you the question

  • Thinks skipIf re-evaluates its condition before each test
  • Passes a function object as the condition instead of calling it
  • Expects a decorator condition to see attributes set in setUp
  • Confuses skipIf and skipUnless polarity
  • Believes self.skipTest marks the test as passed

context