Write tests/test_models.py — comprehensive coverage for Prompt and PromptLibrary
Day 68 of 80
In professional Python projects, tests live in a dedicated tests/ directory at the root of the project. Create it now:
cd prompt-vault
mkdir tests
touch tests/__init__.py
__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.prompt-vault/
├── models.py
├── exceptions.py
├── api.py
├── main.py
└── tests/
├── __init__.py
├── test_models.py ← you build this today
└── test_api.py ← tomorrow
Create tests/test_models.py. This file uses pytest's class grouping to organize tests by what they're testing:
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"
TestPrompt and TestPromptLibrary. Notice that TestPromptLibrary methods accept a tmp_path parameter — that's a pytest fixture explained below.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:
Prompt tests together, all PromptLibrary tests togetherTestPrompt::test_create_valid_prompt, which reads like plain Englishsetup_method or class-level fixture later if neededNote: pytest class methods still take self as first argument, but you don't inherit from unittest.TestCase — these are plain classes, not unittest classes.
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.
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
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.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.
$ 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 ===================
Prompt or change how PromptLibrary handles persistence, these tests will catch any regressions instantly.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:
Prompt class doesn't strip/capitalize platform the same way the test expectspreview() method counts characters differently than assumedpytest from the prompt-vault/ root__init__.py in the tests/ folderRun with extra detail on a specific test to debug it:
pytest tests/test_models.py::TestPrompt::test_preview_long -v -s
-s flag disables output capture — any print() statements in your code will show up in the terminal. Useful when debugging a specific failing test.tests/ directory with __init__.py inside the prompt-vault projecttests/test_models.py with TestPrompt and TestPromptLibrary classespytest tests/ -vtmp_path fixture and why it prevents test pollutionpytest path::Class::methodprompts.jsonDay 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.