Week 14 of 16

Test the Prompt Class

Write tests/test_models.py — comprehensive coverage for Prompt and PromptLibrary

Day 68 75 minutes Build

Day 68 of 80

Create the tests/ Folder

In professional Python projects, tests live in a dedicated tests/ directory at the root of the project. Create it now:

terminal bash
cd prompt-vault
mkdir tests
touch tests/__init__.py
The __init__.py file (empty is fine) makes tests/ a Python package, which helps pytest and your imports work correctly. Some projects skip this file — adding it is the safer default.
Project Structure After Today
prompt-vault/
├── models.py
├── exceptions.py
├── api.py
├── main.py
└── tests/
    ├── __init__.py
    ├── test_models.py     ← you build this today
    └── test_api.py        ← tomorrow

The Full test_models.py

Create tests/test_models.py. This file uses pytest's class grouping to organize tests by what they're testing:

tests/test_models.py python
import pytest
from models import Prompt, PromptLibrary
from exceptions import InvalidPlatformError, EmptyFieldError


class TestPrompt:

    def test_create_valid_prompt(self):
        p = Prompt("Kling", "Golf swing", "Cinematic slow motion golf swing")
        assert p.platform == "Kling"
        assert p.shot == "Golf swing"

    def test_platform_cleaned(self):
        p = Prompt("  kling  ", "test", "test prompt")
        assert p.platform == "Kling"

    def test_invalid_platform_raises_error(self):
        with pytest.raises(InvalidPlatformError):
            Prompt("Pika", "test", "test prompt")

    def test_empty_shot_raises_error(self):
        with pytest.raises(EmptyFieldError):
            Prompt("Kling", "", "test prompt")

    def test_str_representation(self):
        p = Prompt("Runway", "Ocean waves", "Aerial tracking shot")
        assert str(p) == "[Runway] Ocean waves"

    def test_preview_long(self):
        long_text = "A" * 100
        p = Prompt("Kling", "test", long_text)
        preview = p.preview(50)
        assert len(preview) == 53  # 50 + "..."
        assert preview.endswith("...")

    def test_to_dict_roundtrip(self):
        original = Prompt("Kling", "Golf swing", "Cinematic slow motion")
        data = original.to_dict()
        restored = Prompt.from_dict(data)
        assert restored.platform == original.platform
        assert restored.shot == original.shot


class TestPromptLibrary:

    def test_empty_library(self, tmp_path):
        lib = PromptLibrary(tmp_path / "test.json")
        assert len(lib) == 0

    def test_add_and_retrieve(self, tmp_path):
        lib = PromptLibrary(tmp_path / "test.json")
        lib.add(Prompt("Kling", "Golf swing", "Cinematic"))
        assert len(lib) == 1

    def test_persistence(self, tmp_path):
        path = tmp_path / "test.json"
        lib1 = PromptLibrary(path)
        lib1.add(Prompt("Runway", "Ocean", "Aerial tracking shot"))
        lib2 = PromptLibrary(path)
        assert len(lib2) == 1
        assert lib2.prompts[0].platform == "Runway"

    def test_search(self, tmp_path):
        lib = PromptLibrary(tmp_path / "test.json")
        lib.add(Prompt("Kling", "Golf swing", "Slow motion golf"))
        lib.add(Prompt("Runway", "Ocean waves", "Aerial ocean tracking"))
        results = lib.search("golf")
        assert len(results) == 1

    def test_delete(self, tmp_path):
        lib = PromptLibrary(tmp_path / "test.json")
        lib.add(Prompt("Kling", "shot 1", "prompt 1"))
        lib.add(Prompt("Runway", "shot 2", "prompt 2"))
        lib.delete(0)
        assert len(lib) == 1
        assert lib.prompts[0].platform == "Runway"
Two test classes: TestPrompt and TestPromptLibrary. Notice that TestPromptLibrary methods accept a tmp_path parameter — that's a pytest fixture explained below.

Why Use Test Classes?

Grouping for Organization

You don't have to use classes in pytest — plain functions work perfectly. But when testing a complex model like Prompt, grouping related tests in a class gives you:

Note: pytest class methods still take self as first argument, but you don't inherit from unittest.TestCase — these are plain classes, not unittest classes.

The tmp_path Fixture

What a Fixture Is

A fixture is something pytest sets up and tears down for your test. You request a fixture by adding a parameter with its name to your test function. pytest sees the parameter name, recognizes it as a known fixture, and provides it automatically.

tmp_path is a built-in pytest fixture that gives you a temporary directory. It's unique per test and automatically deleted after the test finishes.

tmp_path in action python
def test_persistence(self, tmp_path):
    # tmp_path is a pathlib.Path pointing to a fresh temp directory
    path = tmp_path / "test.json"  # doesn't exist yet

    lib1 = PromptLibrary(path)      # creates the file
    lib1.add(Prompt("Runway", "Ocean", "Aerial shot"))

    lib2 = PromptLibrary(path)      # reads from the same file
    assert len(lib2) == 1           # persistence verified
    # temp directory cleaned up automatically after test ends
Without tmp_path, your tests would write to your actual prompts.json file. That would pollute your data, cause tests to interfere with each other, and make results depend on existing data. The fixture solves all of this cleanly.
Never Write to Real Files in Tests

Tests should be independent and side-effect free. If a test writes to prompts.json, it changes the state of your app for the next test (and for yourself when you go back to using the app). Always use tmp_path for any file I/O in tests.

Run the Tests

terminal — from prompt-vault root bash
$ pytest tests/ -v

tests/test_models.py::TestPrompt::test_create_valid_prompt PASSED     [  8%]
tests/test_models.py::TestPrompt::test_platform_cleaned PASSED        [ 16%]
tests/test_models.py::TestPrompt::test_invalid_platform_raises_error PASSED [ 25%]
tests/test_models.py::TestPrompt::test_empty_shot_raises_error PASSED [ 33%]
tests/test_models.py::TestPrompt::test_str_representation PASSED      [ 41%]
tests/test_models.py::TestPrompt::test_preview_long PASSED            [ 50%]
tests/test_models.py::TestPrompt::test_to_dict_roundtrip PASSED       [ 58%]
tests/test_models.py::TestPromptLibrary::test_empty_library PASSED    [ 66%]
tests/test_models.py::TestPromptLibrary::test_add_and_retrieve PASSED [ 75%]
tests/test_models.py::TestPromptLibrary::test_persistence PASSED      [ 83%]
tests/test_models.py::TestPromptLibrary::test_search PASSED           [ 91%]
tests/test_models.py::TestPromptLibrary::test_delete PASSED           [100%]

=================== 12 passed in 0.18s ===================
12 green. Your core data models are verified. When you refactor Prompt or change how PromptLibrary handles persistence, these tests will catch any regressions instantly.

What If a Test Fails?

Debugging Failing Tests

If a test fails, read the output carefully: it shows you the exact line, the expected value, and what you actually got. Don't guess — read. The most common causes:

Run with extra detail on a specific test to debug it:

terminal — debug specific test bash
pytest tests/test_models.py::TestPrompt::test_preview_long -v -s
The -s flag disables output capture — any print() statements in your code will show up in the terminal. Useful when debugging a specific failing test.

End of Day Checklist

Tomorrow

Day 69 adds tests/test_api.py using FastAPI's TestClient. You'll test HTTP endpoints — list, create, validate, stats — without starting a real server. The pattern is nearly identical to what you built today.