Skip to content

Creating a Test Plan

A test plan is an ordinary pytest project that loads the f3ts plugin. This page covers the recommended file layout and how to scaffold a new plan.

Minimal layout

The smallest plan is two files in one directory:

my_test_plan/
├── config.yml      # test-case limits + runner settings (required)
└── test_plan.py    # the test cases

That's enough to run offline (see Your First Test).

As a plan grows — real hardware, shared setup, helper modules — split it up. This mirrors how production FixturFab test plans are organized:

my_test_plan/
├── config.yml       # test-case limits, runner + fixture settings
├── conftest.py      # pytest fixtures (open/close the hardware interface)
├── interface.py     # the class that controls the test hardware / DUT
├── test_plan.py     # the test cases
├── hw_modules/      # (optional) drivers for instruments, GPIO, etc.
└── pyproject.toml   # dependencies (pins pytest-f3ts)
  • config.yml — limits and metadata for each test case. See Configuration.
  • interface.py — one class that owns all hardware access. See The Test Interface.
  • conftest.py — pytest fixtures, most importantly the session-scoped interface fixture that opens the hardware once and tears it down at the end.
  • test_plan.py — the tests. They pull in interface, test_config, and any f3ts fixtures they need.

Scaffold a new plan

  1. Create a directory and a Poetry project:
    bash
    mkdir my_test_plan && cd my_test_plan
    poetry init
    poetry add pytest-f3ts
  2. Add a config.yml (must include test_name, runner_settings, and test_cases):
    yaml
    test_name: "my-test-plan"
    runner_settings: {}
    test_cases:
      test_power_on:
        test_id: "1.1"
        description: "DUT powers on"
  3. Add an interface.py with a class that controls your hardware:
    python
    import logging
    
    logger = logging.getLogger(__name__)
    
    
    class TestInterface:
        """Controls the test hardware / device under test."""
    
        def __init__(self):
            # Open serial ports, VISA instruments, GPIO, etc.
            pass
    
        def close(self):
            # Close ports / power down.
            pass
  4. Add a conftest.py exposing the interface as a session fixture:
    python
    import pytest
    
    from interface import TestInterface
    
    
    @pytest.fixture(scope="session")
    def interface(request):
        dut = TestInterface()
        request.addfinalizer(dut.close)
        return dut
  5. Add a test_plan.py with your first test:
    python
    def test_power_on(interface):
        assert interface is not None

Running the test plan

From the plan directory:

bash
pytest -p f3ts

See Running Locally for the offline workflow, and With the Test Runner for running under the FixturFab Test Runner.

A complete, runnable version of this layout (with a mock interface, so it passes with no hardware) is documented in the Starter Test Plan.