Customize test generation

To get started with generating tests, read Generate tests

image

Customizing Kolo’s test generation works by programmatically modifying the test plan with step or plan hooks.

For every test that Kolo generates from a trace, it first generates a test plan. And then b on that test plan the test code is generated. The test plan is a list of steps in JSON format (see the left side of the screenshot above)

View a list of all available steps: Test generation steps

Test generation step hooks

Kolo’s test generation works by generating a series of “steps”. Each of these steps represents a bit of the rendered test. Kolo provides the step_hook decorator which allows a step to be customised. For example, you could define a step hook that replaces a ModelCreate step with a FactoryCreate step:

from kolo import step_hook
from kolo.generate_tests.steps import ModelCreate, FactoryCreate


@step_hook(ModelCreate)
def model_create_to_factory(model_create):
    if model_create.module == "django.contrib.auth.models" and model_create.model == "User":
        factory_create = FactoryCreate(
            module="tests.factories",
            factory="UserFactory",
            fields=model_create.fields,
            defines_variable_name=model_create.defines_variable_name,
        )
        return [factory_create]
    return [model_create]

In the above code block, we received the original step (model_create) in our hook. Kolo then expects us to return a list of steps we’d like to replace that step with. In the case of our user model we’d like to replace it with just a single step, so we do return [factory_create].


Some step types come in two parts representing an indented block of code. For example, the TestFunction step is paired with the EndTestFunction step. A step hook for TestFunction will be passed a list of steps starting with a TestFunction, followed by all the steps for that test and finally an EndTestFunction step:

from kolo import step_hook
from kolo.generate_tests.steps import Code, EndTestFunction, TestFunction


@step_hook(TestFunction)
def add_pytest_fixture(steps):
    assert isinstance(steps[0], TestFunction)
    assert isinstance(steps[-1], EndTestFunction)

    steps[0].fixtures += ("my_cool_fixture",)
    steps.insert(1, Code("my_cool_fixture.do_something()"))
    return steps

Test generation plan hooks

It is also possible to define a hook that can work on the entire test plan. A plan hook takes a Plan instance and should return a new or modified Plan:

from kolo import plan_hook
from kolo.generate_tests.steps import EndTimeTravel, Code, StartTimeTravel, TestFunction


@plan_hook
def time_machine_decorator(plan):
    """
    Replace a time travel context manager with a decorator.

    First we find the `StartTimeTravel` step. If there are zero or
    multiple time travel steps, exit early without modifying the plan.

    Next we build up a new list of steps. This new list omits the
    `StartTimeTravel` and `EndTimeTravel` steps. It also inserts a new
    `Code` step defining a time travel decorator just before the
    `TestFunction` step.

    Finally the new steps are attached to the plan and the plan is
    returned.
    """
    time_travel_step = None
    for step in plan.steps:
        if isinstance(step, StartTimeTravel):
            if time_travel_step is None:
                time_travel_step = step
            else:
                # If there are two time travel context managers leave them both alone
                return plan

    if time_travel_step is None:
        # If there are no time travel context managers we have nothing to do
        return plan

    new_steps = []
    for step in plan.steps:
        if isinstance(step, (StartTimeTravel, EndTimeTravel)):
            pass
        elif isinstance(step, TestFunction):
            args = ", ".join(time_travel_step.args)
            time_travel_decorator = Code(
                f"@{time_travel_step.call}({args})",
                time_travel_step.imports,
            )
            new_steps.append(time_travel_decorator)
            new_steps.append(step)
        else:
            new_steps.append(step)

    plan.steps = new_steps
    return plan

Ensuring hooks are imported

Whether you’re using step hooks or plan hooks, you can ensure these are imported by Kolo by setting test_generation.hook_imports in .kolo/config.toml:

# .kolo/config.toml

[test_generation]

hook_imports = ["path.to.hooks.module", "path.to.more.hooks"]

Field parsers

Kolo supports many model fields provided by Django. Sometimes you may have a third-party field which isn’t being parsed correctly. In this case you can define a custom field parser:

def parse_custom_field(value, field):
    if field != "dotted.field.path.CustomField":
        return value

    # custom parsing logic

    return parsed_value
# .kolo/config.toml

[test_generation]

field_parsers = [
    "path.to.parse_custom_field",
]