Skip to content

Guidelines for Writing Tests

File Structure

Each test file tests all classes and functions in its corresponding source file. The structure mirrors src/rendercv/:

src/rendercv/renderer/templater/date.py
    → tests/renderer/templater/test_date.py
    (tests all functions and classes in date.py)

src/rendercv/schema/models/cv/section.py
    → tests/schema/models/cv/test_section.py
    (tests all functions and classes in section.py)

Naming Conventions

Test names must include the name of the function or class being tested.

When you need only one test, use test_ + the name:

  • Testing clean_url()test_clean_url
  • Testing Cvtest_cv

When you need multiple tests, wrap them in a class using Test + PascalCase name:

  • Testing clean_url()TestCleanUrl
  • Testing CvTestCv

Example with one test:

@pytest.mark.parametrize(
    ("url", "expected_clean_url"),
    [
        ("https://example.com", "example.com"),
        ("https://example.com/", "example.com"),
        ("https://example.com/test", "example.com/test"),
    ],
)
def test_clean_url(url, expected_clean_url):
    assert clean_url(url) == expected_clean_url

Example with multiple tests:

class TestComputeDateString:
    @pytest.mark.parametrize(...)
    def test_date_parameter_takes_precedence(self, ...):
        ...

    @pytest.mark.parametrize(...)
    def test_date_ranges(self, ...):
        ...

    @pytest.mark.parametrize(...)
    def test_returns_none_for_incomplete_data(self, ...):
        ...

Use Parametrize for Variations

Instead of writing multiple similar tests, use @pytest.mark.parametrize:

@pytest.mark.parametrize(
    ("input_a", "input_b", "expected"),
    [
        ("2020-01-01", "2021-01-01", "Jan 2020 – Jan 2021"),
        ("2020-01", "2021-02-01", "Jan 2020 – Feb 2021"),
        (2020, 2021, "2020 – 2021"),
    ],
)
def test_date_ranges(self, input_a, input_b, expected):
    result = compute_date_string(None, input_a, input_b, EnglishLocale())
    assert result == expected

Shared Fixtures with conftest.py

Place shared fixtures in conftest.py. Use the closest one possible:

  • Fixtures for one folder → that folder's conftest.py
  • Fixtures for multiple folders → their closest common parent's conftest.py
tests/
├── conftest.py                    # Used across all tests
├── schema/
│   ├── conftest.py                # Used by schema tests only
│   └── models/
│       └── cv/
│           ├── conftest.py        # Used by CV model tests only
│           ├── test_section.py
│           └── test_cv.py
└── renderer/
    └── ...

Testing Principles

Keep tests focused. Test functions in isolation: input → output.

Don't create unnecessary fixtures. If setup is one clear line, inline it:

# Don't:
@pytest.fixture
def locale(self):
    return EnglishLocale()

def test_something(self, locale):
    result = format_date(Date(2020, 1, 1), locale)

# Do:
def test_something(self):
    result = format_date(Date(2020, 1, 1), EnglishLocale())

Prefer real behavior over mocking. Only mock when there's no practical alternative (external APIs, file system, etc.).

Name tests by expected behavior, not by input:

  • Good: test_returns_none_for_incomplete_data - describes what should happen
  • Bad: test_function_with_none_input - describes input but not behavior

Keep tests simple:

def test_something(self, input, expected):
    result = function_under_test(input)
    assert result == expected

What to test:

  • Input → expected output
  • Input → expected error

What to avoid:

  • Testing implementation details instead of behavior
  • Complex test setup when simple values work