Auto-generating unit tests in Claude Code
Claude Code can write tests on its own, but the result depends on the model and the prompt. How to connect it to zukko.pro and get real tests, not filler.
4 min · · Updated · Claude Code · тестирование
Connecting
Claude Code is an Anthropic-compatible client, so the gateway address is set without /v1 — it appends the path itself. There are two ways to set the variables: through the environment or through settings.json in ~/.claude/.
Via environment:
export ANTHROPIC_BASE_URL=https://api.zukko.pro
export ANTHROPIC_AUTH_TOKEN=fetch_your_keyVia settings.json:
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.zukko.pro",
"ANTHROPIC_AUTH_TOKEN": "fetch_your_key"
}
}Use ANTHROPIC_AUTH_TOKEN, not ANTHROPIC_API_KEY — with the latter Claude Code assumes you're signed in through an official Anthropic account and asks you to log in instead of using the key. Keys are created in the dashboard at https://zukko.pro/app/api-keys and shown once, so save it to a password manager right away.
Which model to default to
For test generation it helps to split tasks by complexity. claude-sonnet-5 is fast and accurate enough for routine work: tests for getters, input validation, simple error handlers. claude-opus-5-5 is worth trying for code with complex branching, where edge cases need real thought rather than just mirroring the function's structure. You can turn on extended thinking for it — check on your own complex modules whether that cuts down the number of tests that pass but check nothing. Price-wise both models are the same on Zukko ($0.30/$0.30 per 1M input/output tokens).
Both models hold a 1,000,000-token context, so you can feed in not just the file under test but neighboring modules, existing tests for style, and the test runner config — the model writes tests that match the project's conventions instead of inventing its own mocking style.
Switching models in Claude Code is done with /model claude-opus-5-5 in a session, or a startup flag.
A prompt that doesn't produce useless assertions
Left unconstrained, models tend to generate tests like expect(result).toBeDefined() — technically a test, but it catches nothing. A prompt that works usually applies three constraints at once: an explicit ban on assertions without concrete values, a requirement to cover at least one edge case, and a requirement to look at existing tests in the repo as a template.
Example request inside a Claude Code session:
Write unit tests for src/payments/calculateFee.ts.
Requirements:
- every test checks a specific numeric value or a specific error, no toBeDefined/toBeTruthy without reason
- a separate test for a zero amount and for a negative amount (should throw ValidationError)
- style and structure should match src/payments/__tests__/calculateTax.test.ts
- use Vitest, same as the rest of the project
A prompt like this, combined with a neighboring test file as context, usually produces tests that actually fail if you break the logic — worth checking by corrupting one line in the function and rerunning the suite.
When generation cuts off
If the response stops mid-file, it is almost never a token limit — it's a client or network timeout. Check max_tokens in your config and the HTTP client's timeout if you're calling the model directly through an SDK inside your own script rather than through Claude Code's built-in agent. Claude Code itself sets reasonable timeouts by default; cutoffs there usually happen on very long files, so it makes sense to ask for tests one module at a time — the 1M-token context is generous enough that you won't lose the project's overall picture even when splitting the work up this way.
A regression test from a bug screenshot
Since the Claude models (like GPT) accept images, a bug screenshot can be attached directly to the request instead of a text description. The model sees exactly what's broken in the interface and generates a test that reproduces it:
import base64, requests
with open("bug-screenshot.png", "rb") as f:
img = base64.b64encode(f.read()).decode()
resp = requests.post(
"https://api.zukko.pro/v1/messages",
headers={"Authorization": "Bearer fetch_your_key", "anthropic-version": "2023-06-01"},
json={
"model": "claude-opus-5-5",
"max_tokens": 2048,
"messages": [{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": img}},
{"type": "text", "text": "The screenshot shows a bug in the payment form. Write a Playwright regression test that catches it."},
],
}],
},
)This works better than describing the bug in words: the model sees the exact state of the form rather than a human's interpretation of it.
When the model your CI depends on gets switched off
The model catalogue updates from time to time, and a model hardcoded into a CI config can end up switched off — that has already happened to claude-opus-4.6, for instance. A request to it returns 503 with the model_disabled code (not a 404 and not model_retired):
{"error": {"code": "model_disabled"}}Look up the replacement in the current GET https://api.zukko.pro/v1/models list — for Opus it's claude-opus-5-5. It's worth handling this code separately from other errors and logging it — then updating the CI config takes one line, not an investigation into why every test-generation run suddenly started failing.
Keeping track of cost
Every regular (non-streaming) gateway response carries an X-Zukko-Cost-Micros header — the cost of that specific request in microdollars (1,000,000 = $1) — and X-Fetch-Balance-Cents with the remaining balance after the charge (despite the name, also in microdollars). Streaming responses — which is how Claude Code itself works — don't carry these headers: check Claude Code session spend in the dashboard, via GET /v1/usage or GET /v1/balance. If you've automated running test generation across every module in a project with a non-streaming script, it's worth logging these headers to see which files the model "thinks" longer and more expensively on — usually a sign the logic there is complex and the generated tests deserve a manual check.
Full spend history (shared across the whole account, all keys together) is available through GET /v1/usage with cursor pagination, and the current balance through GET /v1/balance. Both require a key:
curl https://api.zukko.pro/v1/balance \
-H "Authorization: Bearer fetch_your_key"If the balance runs out, the gateway returns 402 before ever reaching the model — the request costs nothing, but you also get no tests. For a CI pipeline that runs test generation on every PR, it's worth setting a daily spending limit on the key from its page in the dashboard, so one runaway loop doesn't burn through the whole budget at once.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- opencode agents: setting up roles, models and toolsA custom agent in opencode is a role, a model and a set of permissions. A working config where a cheap model handles routine work and a stronger one edits code.
- OpenAI API Keys in the Open: Why You Should Not Use ThemWhere free keys on GitHub come from, why using them is dangerous for you and for the owner, and how to protect your own key from the same leak.
- GLM API Errors: Why the Request Fails and How to Fix ItA wrong model id, an overflowing 128K context, the key, the balance, limits: the typical causes of GLM API errors and a quick way to find yours.
- Plan mode in Claude Code: plan first, then edit the codeIn plan mode Claude Code reads your code and proposes a plan but edits nothing until you agree. How to enable it, use it and what it costs.