ChatGPT Prompt to Write Unit Tests for a Function
Setting the Scene
You have a small Python helper called parse_iso8601_duration. It turns strings like P1Y2M3DT4H5M6S into a datetime.timedelta, and it is used in a scheduling service where a wrong parse means a job fires at the wrong time. A colleague reviews the pull request and asks the question everyone dreads: “where are the tests?” You know you should write them, you know the edge cases are the whole point, and you also know that hand-writing twenty parametrized cases is tedious enough that it keeps getting postponed. That is precisely the gap a structured prompt closes.
The goal here is not for AI to invent a new function. The goal is to make ChatGPT produce a reviewable test file for a contract you already defined, including the nasty inputs (empty strings, duplicate designators, bad ordering) that people forget until production. Below is one prompt you can reuse for any pure function, the fields to change, the output you should expect, and the variations that fit different languages and frameworks.
The Reusable Prompt
Send the block below as a single message. The function contract is the part you edit; everything else is the reusable scaffolding.
You are a senior Python engineer who writes thorough but readable test suites with pytest.
Write unit tests for the function described below. Assume it lives in durations.py.
Function contract:
- Name: parse_iso8601_duration
- Input: a string in ISO-8601 duration format, for example P1Y2M3DT4H5M6S
- Output: a datetime.timedelta
- Supported designators: Y for years, M for months, W for weeks, D for days, then T followed by H, M, S
- Raise ValueError for any malformed or unsupported input
- A missing component defaults to zero
- A leading minus sign indicates a negative duration
Rules for the tests:
1. Use pytest with plain assert statements, no unittest classes.
2. Name every test function so it starts with test_ and states the behavior.
3. Use parametrize for the valid-format cases with at least 8 input and expected pairs.
4. Cover these valid cases: days only, time only, weeks, combined date and time, zero duration, negative duration, fractional seconds.
5. Cover these invalid cases: empty string, no leading P, unknown designator, duplicate designator, components out of order, lowercase designators.
6. Test boundary values: P0D, PT0S, and a span covering several years.
7. Include one test that asserts the return value is a timedelta instance.
8. Add a short comment above each test explaining the behavior it protects.
9. Do not write the implementation. Tests only.
10. End with the exact command to run this file.
Output the full file as one code block, then list every assumption you made about the contract.
The contract block is doing the real work. If you hand the model only a function name, it guesses the semantics and you get tests that assert the wrong behavior. If you hand it a contract with explicit error rules, it generates exactly the malformed-input cases that catch real bugs.
Variables to Swap
| Field | What to change | Example variation |
|---|---|---|
| Function name | Your real symbol | parse_money_cents |
| Input type | What the caller passes | a decimal string like 19.99 |
| Output type | What comes back | an int number of cents |
| Error behavior | Raise, return None, or log | raise ValueError on bad input |
| Framework | pytest, unittest, or Jest | pytest |
| Valid cases | The happy paths you care about | round amounts, zero, large values |
| Invalid cases | The abuse you want blocked | empty, symbols, negative for a count |
Two fields repay extra care. The error behavior drives half the test file, so state it precisely; “handle bad input” is not a spec. And the valid cases list is where your domain knowledge lives, so name the real inputs you see in logs rather than generic placeholders.
Sample Output
Expect a runnable pytest file plus an assumptions list. A trimmed slice of the expected shape:
import pytest
from datetime import timedelta
from durations import parse_iso8601_duration
@pytest.mark.parametrize("value, expected", [
("P1D", timedelta(days=1)),
("PT1H", timedelta(hours=1)),
("P2W", timedelta(weeks=2)),
("P1DT2H3M", timedelta(days=1, hours=2, minutes=3)),
("P0D", timedelta(0)),
("PT0S", timedelta(0)),
("-PT30M", timedelta(minutes=-30)),
("PT1.5S", timedelta(seconds=1.5)),
])
def test_valid_durations_parse(value, expected):
"""protects the happy path across date, time, and mixed formats"""
assert parse_iso8601_duration(value) == expected
@pytest.mark.parametrize("bad", ["", "1Y", "P1X", "P1D1D", "P1M1Y", "p1d"])
def test_invalid_durations_raise(bad):
"""protects the parser from silently accepting malformed input"""
with pytest.raises(ValueError):
parse_iso8601_duration(bad)
def test_returns_timedelta():
"""protects the public return type contract"""
assert isinstance(parse_iso8601_duration("PT1S"), timedelta)
Run with: pytest test_durations.py -v
A good response includes the parametrized valid cases, a separate invalid block, a type assertion, and a run command. The assumptions list is your review checklist: if the model says “assumed months are 30 days,” you either confirm it or correct the contract and rerun.
Design Notes Behind the Prompt
Four choices shape the result. First, the output-type rule (“tests only, no implementation”) stops the model from burying real test logic inside an implementation file. Second, parametrize with a minimum count forces table-driven tests instead of one function per case, which keeps the file short and readable. Third, naming invalid cases explicitly, rather than saying “test bad input,” guarantees the malformed scenarios actually appear. Fourth, the assumptions list turns silent guesses into reviewable statements, which is the difference between trusting a test file and merging it blind.
Pitfalls to Watch
- Tests that only assert no exception. A test that calls the function and asserts nothing else passes even when the value is wrong. Require equality checks against expected outputs.
- Brittle floating-point equality. For
PT1.5Scompare against atimedelta, not a raw float, to avoid rounding surprises. - Over-mocking pure functions. A duration parser has no I/O, so it needs no mocks. Mocks here hide bugs rather than isolate them.
- Copying the implementation into the test. If the test re-implements the logic, it agrees with the bug. Use literal expected values.
- Ignoring the assumptions list. That list often reveals that you and the model disagree about month length or sign handling.
Adaptations
- TypeScript and Jest: change the framework line to Jest and ask for
describeanditblocks withtoThrowfor the invalid cases. - Property-based tests: add “Also propose three Hypothesis properties that should always hold, such as parse of a formatted value round-tripping.”
- Coverage target: add “Ensure branch coverage of every error path and report which branches each test covers.”
- Legacy function: append “The function currently returns None on bad input; write tests for current behavior first, then a separate group marked xfail for the desired behavior.”
- Regression guard: paste a real bug report and add “Write one test that fails on the current code and passes once the bug is fixed.”
To keep the loop tight, run the file, paste any failures back, and ask for the minimal fix. For deeper testing setups, see ChatGPT Settings for Test-Driven Development, and when a test fails for reasons you cannot see, work through the ChatGPT debug workflow. To orient yourself in an unfamiliar module before writing tests, start with this prompt to explain a codebase.
Sources & Notes
Prompt pattern written for ChatGPT (GPT-4o and later) and adaptable to Claude and Gemini. The ISO-8601 duration grammar referenced here follows the public ISO-8601 standard for date and time intervals. Tested behavior: listing explicit invalid inputs produces more edge-case coverage than a general instruction to test errors. No external data or package versions were fetched; the example test file is illustrative and omits the implementation. Confirm the error type and month-length assumption against your own contract before merging the generated tests.
