Tutorials / Framework Features

Shared lifecycle rules

As a project grows, the same setup and teardown logic ends up copy-pasted across every suite. A Rules subclass breaks that cycle — define the shared lifecycle once, attach it to any suite with a single parameter.

The problem: repeated teardown

In a browser project, every suite opens a driver and checks for JavaScript errors after each test. Without shared rules, that code lives in every suite:

suites.pypython
@Suite()
class LoginSuite:
    @beforeClass()
    def setup(self):
        self.driver = new_driver()
    @afterClass()
    def teardown(self):
        Browser.shutdown()
    @afterTest()
    def check_js_errors(self):
        errors = Browser.get_log("browser")
        if errors:
            raise AssertionError(str(errors))

@Suite()
class CheckoutSuite:
    @beforeClass()
    def setup(self):
        self.driver = new_driver()  # identical
    @afterClass()
    def teardown(self):
        Browser.shutdown()  # identical
    @afterTest()
    def check_js_errors(self):
        ...  # identical again

The solution: a Rules subclass

Move the shared logic into a Rules subclass, then reference it from each suite via rules=.

browser_rules.pypython
from test_junkie.rules import Rules

class BrowserRules(Rules):

    def before_class(self):
        self.driver = new_driver()

    def after_class(self):
        Browser.shutdown()

    def after_test(self, **kwargs):
        logs = Browser.get_log("browser")
        errors = [m["message"] for m in logs
                  if m["level"] == "SEVERE"]
        if errors:
            raise AssertionError(str(errors))
suites.pypython
@Suite(rules=BrowserRules)
class LoginSuite:
    @test()
    def test_valid_login(self):
        ...


@Suite(rules=BrowserRules)
class CheckoutSuite:
    @test()
    def test_add_to_cart(self):
        ...

# both suites open/close the browser and check
# JS errors after every test — one definition
Rules coexist with suite-local lifecycle hooks
A suite can have both rules=BrowserRules and its own @beforeClass/@afterClass. Rules hooks run first for before_class/before_test, and last for after_class/after_test. Use Rules for the shared part, and the suite's own hooks for the specific part.

Available lifecycle methods

Override any combination — you don't need all four:

rules_reference.pypython
from test_junkie.rules import Rules

class MyRules(Rules):

    def before_class(self):
        # runs before the suite's @beforeClass
        ...

    def after_class(self):
        # runs after the suite's @afterClass
        ...

    def before_test(self, **kwargs):
        # runs before each @beforeTest
        ...

    def after_test(self, **kwargs):
        # runs after each @afterTest
        ...

Real-world tip: screenshot on failure

A common use of after_test is capturing a screenshot when a test fails. The kwargs argument is passed by Test Junkie but rarely needed in Rules hooks — the test status at teardown time is determined by whether any exception was raised during the test itself.

browser_rules.pypython
class BrowserRules(Rules):

    def after_test(self, **kwargs):
        # always capture screenshot at end of test for CI artifacts
        test_name = type(self).__name__
        self.driver.save_screenshot(f"screenshots/{test_name}.png")

        # then check JS errors — raises if any found, marking test failed
        logs = Browser.get_log("browser")
        errors = [m["message"] for m in logs if m["level"] == "SEVERE"]
        if errors:
            raise AssertionError(str(errors))