In the first article, I showed how to get from zero to passing tests in a few minutes - directives, a POST with a .csx script, and chaining IDs across test cases.

This post skips repeating that path. I won’t walk through extra CRUD steps or another “invalid body returns 400” example - you already have the pattern from Part 1. Instead: environments, how this demo API expects an API key, pre-request scripts that feed variables into .http files, built-in functions for inline values, and retries - both for flaky HTTP responses and for work that finishes asynchronously in the background.

I’m still using MMLib.DummyApi as the demo backend. If you haven’t read Part 1, start there; spin up the API with docker run -p 8080:8080 ghcr.io/burgyn/mmlib-dummyapi.

Environments

Until now, the API base URL was hardcoded in every .http file. That breaks the moment you want to run the same tests against staging. Let’s fix that.

Create .teapie/env.json in your project root:

{
  "$shared": {
    "ApiBaseUrl": "http://localhost:8080",
    "ApiKey": "test-api-key-123"
  },
  "staging": {
    "ApiBaseUrl": "https://my-staging-api.example.com"
  }
}

$shared is the default environment - its variables are always available. staging overrides ApiBaseUrl for that specific environment.

Now update your .http files to use the variable:

## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
GET {{ApiBaseUrl}}/products

Run against local (default):

teapie test Tests/

Run against staging:

teapie test Tests/ -e staging

One test collection, multiple environments.

Of course, tests won’t pass against staging right now - that URL doesn’t exist. Replace it with your real staging endpoint when you have one.

API keys in this demo (and what comes next)

The DummyApi orders endpoints expect an X-Api-Key header. Without it, you get 401. For this sample, put the key in .teapie/env.json (the same file as in Environments). The ApiKey property becomes variable {{ApiKey}} in .http files. That is handy for workshops and Docker demos, but not how every real API handles identity.

A follow-up post will cover other authentication styles and a custom auth provider in TeaPie so you don’t have to paste secrets into every request by hand.

Positive path: use the env value in a header the same way you use {{ApiBaseUrl}}. Create Tests/002-Orders/001-List-Orders-Authorized-req.http:

## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
GET {{ApiBaseUrl}}/orders
X-Api-Key: {{ApiKey}}

ApiKey is resolved from .teapie/env.json when you run teapie test.

Quick negative check - no header, expect rejection. Create Tests/002-Orders/004-Unauthorized-Access-req.http:

## TEST-EXPECT-STATUS: [401]
GET {{ApiBaseUrl}}/orders

Pre-request scripts: set variables, keep .http readable

Sometimes you need to prepare values before TeaPie sends the request. Add a pre-request script next to the test case: same base name, suffix -init.csx. It runs first; use tp.SetVariable for anything the .http file should reference by name.

Example: Tests/002-Orders/002-Create-Order-init.csx:

tp.SetVariable("OrderCustomerId", Guid.NewGuid().ToString());

Http file: Tests/002-Orders/002-Create-Order-init.http:

# @name CreateOrderRequest
## TEST-EXPECT-STATUS: [201]
POST {{ApiBaseUrl}}/orders
Content-Type: application/json
X-Api-Key: {{ApiKey}}

{
  "customerId": "{{OrderCustomerId}}",
  "customerName": "TeaPie Demo Customer",
  "customerEmail": "teapie.demo@example.com",
  "totalAmount": 29.98,
  "status": "pending",
  "shippingAddress": "123 TeaPie Lane",
  "shippingCity": "Demo City",
  "shippingCountry": "SK"
}

002-Create-Order-test.csx (shortened):

await tp.Test("Created order should have a valid ID.", async () =>
{
    dynamic body = await tp.Response.GetBodyAsExpandoAsync();
    True(body.id != null);
    tp.SetVariable("NewOrderId", (string)body.id.ToString());
});

await tp.Test("Order status should start as 'pending'.", async () =>
{
    dynamic body = await tp.Response.GetBodyAsExpandoAsync();
    Equal("pending", (string)body.status);
});

Built-in functions in .http files

TeaPie can inject dynamic values directly in the request file, without an init script. You can use build-in functions; names start with $ and use space-separated arguments (no commas), for example {{$randomInt 1 100}}.

The defaults today are:

Function Role
$guid New GUID
$now Current local time, optional format string
$rand Random double in [0, 1)
$randomInt Random int in [min, max)

You can rewrite preview .http file without -init.csx file.

# @name CreateOrderRequest
## TEST-EXPECT-STATUS: [201]
POST {{ApiBaseUrl}}/orders
Content-Type: application/json
X-Api-Key: {{ApiKey}}

{
  "customerId": "{{$guid}}",
  "customerName": "TeaPie Demo Customer",
  "customerEmail": "teapie.demo@example.com",
  "totalAmount": 29.98,
  "status": "pending",
  "shippingAddress": "123 TeaPie Lane",
  "shippingCity": "Demo City",
  "shippingCountry": "SK"
}

You can also define your own functions and use them from .http files like the built-ins. How that works is worth its own walkthrough - I’ll cover it in the next article.

Retrying when the server answers “not yet”

Sometimes the API is up, but the response is not ready - cold starts, short outages, overloaded instances, or an honest 500. Reasonable clients repeat the request with backoff until they get a successful status (or give up).

DummyApi can force that shape without a real outage. You flip behavior with simulation headers (full list is in the DummyApi README):

Header Effect
X-Simulate-Delay: 500 Add 500 ms delay before responding
X-Simulate-Error: true Return 500
X-Simulate-Retry: 3 Two failing responses, then success on the 3rd
X-Request-Id Correlation id; required when using X-Simulate-Retry

For a worked example, ask for two failures then success, and tell TeaPie to retry until it finally sees 200:

Tests/003-Retry/001-Simulate-Flaky-Get-req.http:

## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
## RETRY-MAX-ATTEMPTS: 5
## RETRY-UNTIL-STATUS: [200]
GET {{ApiBaseUrl}}/products
X-Simulate-Retry: 3
X-Request-Id: {{$guid}}

Without retries, the first responses would be errors. With RETRY-UNTIL-STATUS and enough attempts, the run lines up with the 3rd successful response.

Retrying until the body matches what you expect

In production you often see this: the API returns 200 right away because the request itself was accepted, but the real outcome is produced later - a message on a queue, a background job, a workflow engine, whatever runs asynchronously. Until that work finishes, the resource still looks “in progress”. Your test needs to prove that the job eventually reached the expected state - not that the first HTTP response was OK.

DummyApi does the same with orders: status advances in the background (pendingprocessingcompleted). Your GET /orders/{id} can keep returning 200 while the JSON still says pending, so ## RETRY-UNTIL-STATUS: [200] tells you nothing new - you already had 200 on the first try.

What you need is retry until a post-response test passes. Put the real condition in .csx (here: status is completed), then point the directive at that test’s exact name:

Tests/002-Orders/003-Check-Order-Status-req.http:

# @name CheckOrderStatusRequest
## RETRY-UNTIL-TEST-PASS: Order should eventually reach 'completed' status.
## RETRY-MAX-ATTEMPTS: 15
## RETRY-BACKOFF-TYPE: Linear
GET {{ApiBaseUrl}}/orders/{{NewOrderId}}
X-Api-Key: {{ApiKey}}

003-Check-Order-Status-test.csx:

await tp.Test("Order should eventually reach 'completed' status.", async () =>
{
    dynamic body = await tp.Response.GetBodyAsExpandoAsync();
    Equal("completed", (string)body.status);
});

The string in RETRY-UNTIL-TEST-PASS must match the first argument of tp.Test character for character.

Where you are now

You have:

  • Environments via .teapie/env.json and teapie test … -e <name>
  • Pre-request scripts that set variables consumed from .http files
  • Built-in $… functions for inline dynamic values
  • Retries for flaky status codes and for async state using RETRY-UNTIL-TEST-PASS, with the retrying documentation for deeper configuration

The next article goes deeper: custom test directives, custom auth providers, named retry strategies, reporting, and TeaPie’s AI-assisted workflows.