> ## Documentation Index
> Fetch the complete documentation index at: https://easyaf.nimbleapps.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing with .http Files

> Write API tests using Visual Studio's .http file format with variables, environments, and request chaining.

The `.http` file format lets you define HTTP requests in a readable text format. Breakdance's DotHttp library
provides a runtime and source generator to execute these files as unit tests, with full support for variables,
environments, and request chaining.

## Prerequisites

**1. Install the package**

Add the Breakdance DotHttp package to your test project:

```bash theme={"dark"}
dotnet add package Breakdance.DotHttp
```

**2. Create a .http file**

Add a `.http` file to your project. Visual Studio and VS Code provide syntax highlighting.

## Quick Start

Create a test class that inherits from `DotHttpTestBase`:

```csharp ApiTests.cs theme={"dark"}
using CloudNimble.Breakdance.DotHttp;
using CloudNimble.Breakdance.DotHttp.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net.Http;
using System.Threading.Tasks;

[TestClass]
public class ApiTests : DotHttpTestBase
{
    [TestMethod]
    public async Task GetUsers_ReturnsSuccess()
    {
        SetVariable("baseUrl", "https://jsonplaceholder.typicode.com");

        var request = new DotHttpRequest
        {
            Method = "GET",
            Url = "{{baseUrl}}/users",
            Name = "getUsers"
        };

        var response = await SendRequestAsync(request);

        Assert.IsTrue(response.IsSuccessStatusCode);
    }
}
```

> [!TIP]
>
> While you can manually construct `DotHttpRequest` objects, the real power comes from parsing `.http` files
> or using the source generator to create tests automatically.

## The .http File Format

A `.http` file contains one or more HTTP requests separated by `###`:

```http api.http theme={"dark"}
### Get all users
GET https://api.example.com/users
Accept: application/json

### Create a user
POST https://api.example.com/users
Content-Type: application/json

{
    "name": "John Doe",
    "email": "john@example.com"
}

### Get a specific user
GET https://api.example.com/users/1
```

Each request section includes:

* An optional comment line starting with `#` or `//`
* The HTTP method and URL
* Optional headers (key: value format)
* A blank line separating headers from body
* An optional request body

## Variables

Variables make your requests dynamic and reusable.

### Defining Variables

Define variables at the top of your `.http` file using `@variable = value`:

```http theme={"dark"}
@baseUrl = https://api.example.com
@apiKey = my-secret-key

### Use variables in requests
GET {{baseUrl}}/users
Authorization: Bearer {{apiKey}}
```

### Setting Variables in Code

```csharp theme={"dark"}
[TestMethod]
public async Task GetUsers_WithApiKey()
{
    SetVariable("baseUrl", "https://api.example.com");
    SetVariable("apiKey", Configuration["ApiKey"]); // From test config

    var response = await SendRequestAsync(request);
    Assert.IsTrue(response.IsSuccessStatusCode);
}
```

### Dynamic Variables

Breakdance supports dynamic variables that generate values at runtime:

| Variable | Description | Example Output |
| - | - | - |
| `{{$guid}}` | New GUID | `550e8400-e29b-41d4-a716-446655440000` |
| `{{$datetime}}` | UTC datetime (ISO 8601) | `2024-01-15T10:30:00Z` |
| `{{$localDatetime}}` | Local datetime with timezone | `2024-01-15T10:30:00-05:00` |
| `{{$timestamp}}` | Unix timestamp | `1705315800` |
| `{{$randomInt}}` | Random integer | `42987` |
| `{{$randomInt 100}}` | Random 0-100 | `73` |
| `{{$randomInt 10 50}}` | Random 10-50 | `34` |
| `{{$processEnv VAR}}` | Environment variable | Value of \$VAR |

```http theme={"dark"}
### Create unique resource
POST {{baseUrl}}/items
Content-Type: application/json

{
    "id": "{{$guid}}",
    "timestamp": "{{$datetime}}",
    "sequence": {{$randomInt 1000}}
}
```

### DateTime Formatting and Offsets

Dynamic datetime variables support formatting and offsets:

```http theme={"dark"}
### Datetime with format
# ISO 8601 (default)
GET {{baseUrl}}/reports?from={{$datetime}}

# RFC 1123 format
GET {{baseUrl}}/reports?from={{$datetime rfc1123}}

# Custom format
GET {{baseUrl}}/reports?from={{$datetime "dd-MM-yyyy"}}

### Datetime with offset
# Tomorrow
GET {{baseUrl}}/reports?until={{$datetime 1 d}}

# One week ago
GET {{baseUrl}}/reports?from={{$datetime -7 d}}

# In 1 hour
GET {{baseUrl}}/tokens?expires={{$timestamp 1 h}}
```

**Offset units:** `ms` (milliseconds), `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks), `M` (months), `y` (years)

## Environment Files

Store environment-specific variables in `http-client.env.json`:

```json http-client.env.json theme={"dark"}
{
    "$shared": {
        "apiVersion": "v2"
    },
    "dev": {
        "baseUrl": "https://localhost:5001",
        "apiKey": "dev-key-123"
    },
    "staging": {
        "baseUrl": "https://staging.api.example.com",
        "apiKey": "staging-key-456"
    },
    "prod": {
        "baseUrl": "https://api.example.com",
        "apiKey": "prod-key-789"
    }
}
```

Load environments in your tests:

```csharp theme={"dark"}
[TestInitialize]
public void Setup()
{
    LoadEnvironment("http-client.env.json", "dev");
}

[TestMethod]
public async Task CanSwitchEnvironments()
{
    // Start with dev
    LoadEnvironment("http-client.env.json", "dev");
    var devResponse = await SendRequestAsync(request);

    // Switch to staging
    SwitchEnvironment("staging");
    var stagingResponse = await SendRequestAsync(request);
}
```

### User Overrides

Keep secrets out of source control with `.user` files:

```json http-client.env.json.user theme={"dark"}
{
    "dev": {
        "apiKey": "my-personal-dev-key"
    }
}
```

```csharp theme={"dark"}
LoadEnvironmentWithOverrides(
    "http-client.env.json",
    "http-client.env.json.user",
    "dev");
```

> [!WARNING]
>
> Add `*.user` files to your `.gitignore` to prevent committing secrets.

## Request Chaining

Chain requests together by capturing responses and referencing them in subsequent requests.

### Naming Requests

Use `# @name` to give a request a name for later reference:

```http theme={"dark"}
### Login to get a token
# @name login
POST {{baseUrl}}/auth/login
Content-Type: application/json

{"username": "test", "password": "secret"}

### Use the token from login
GET {{baseUrl}}/users/me
Authorization: Bearer {{login.response.body.$.token}}
```

### Response Reference Syntax

| Reference | Description |
| - | - |
| `{{name.response.body.*}}` | Entire response body |
| `{{name.response.body.$.path}}` | JSONPath extraction |
| `{{name.response.body./xpath}}` | XPath for XML |
| `{{name.response.headers.HeaderName}}` | Response header value |
| `{{name.request.body.*}}` | Original request body |

### JSONPath Examples

```http theme={"dark"}
### Login request
# @name login
POST {{baseUrl}}/auth
Content-Type: application/json

{"username": "admin", "password": "secret"}

### Get user profile using extracted userId
GET {{baseUrl}}/users/{{login.response.body.$.user.id}}
Authorization: Bearer {{login.response.body.$.token}}

### Create order with user's default address
# @name createOrder
POST {{baseUrl}}/orders
Content-Type: application/json
X-Request-Id: {{login.response.headers.X-Request-Id}}

{
    "userId": "{{login.response.body.$.user.id}}",
    "items": [{"productId": "123", "quantity": 1}]
}
```

### Chaining in Code

```csharp theme={"dark"}
[TestMethod]
public async Task ChainedRequests_UseResponseData()
{
    // First request - login
    var loginRequest = new DotHttpRequest
    {
        Method = "POST",
        Url = "{{baseUrl}}/auth/login",
        Name = "login",
        Body = "{\"username\": \"test\", \"password\": \"secret\"}",
        Headers = { ["Content-Type"] = "application/json" }
    };

    await SendRequestAsync(loginRequest);

    // Second request - uses token from login response
    var profileRequest = new DotHttpRequest
    {
        Method = "GET",
        Url = "{{baseUrl}}/users/me",
        Headers = {
            ["Authorization"] = "Bearer {{login.response.body.$.token}}"
        }
    };

    var response = await SendRequestAsync(profileRequest);
    Assert.IsTrue(response.IsSuccessStatusCode);
}
```

## Smart Assertions

Go beyond simple status code checking with `DotHttpAssertions`:

```csharp theme={"dark"}
using CloudNimble.Breakdance.DotHttp;

[TestMethod]
public async Task ValidateApiContract()
{
    var response = await SendRequestAsync(request);

    // Comprehensive validation in one call
    await DotHttpAssertions.AssertValidResponseAsync(response,
        checkStatusCode: true,      // Verify 2xx status
        checkContentType: true,     // Ensure Content-Type header present
        checkBodyForErrors: true,   // Detect error patterns in 200 responses
        logResponseOnFailure: true);// Include body in error messages
}

[TestMethod]
public async Task AssertSpecificConditions()
{
    var response = await SendRequestAsync(request);

    // Check specific status code
    await DotHttpAssertions.AssertStatusCodeAsync(response, 201);

    // Verify Content-Type
    DotHttpAssertions.AssertContentType(response, "application/json");

    // Check for specific header
    DotHttpAssertions.AssertHeader(response, "X-Request-Id");
    DotHttpAssertions.AssertHeader(response, "Cache-Control", "no-store");

    // Verify body contains expected text
    await DotHttpAssertions.AssertBodyContainsAsync(response, "\"success\":true");

    // Detect hidden errors (200 OK with error payload)
    await DotHttpAssertions.AssertNoErrorsInBodyAsync(response);
}
```

### Error Pattern Detection

`AssertNoErrorsInBodyAsync` catches common API anti-patterns where an error is returned with a 200 status:

```json theme={"dark"}
// This 200 OK response would fail the assertion
{
    "error": "User not found",
    "success": false
}
```

Detected patterns:

* `"error":` or `"errors":` fields
* `"success":false` or `"status":"error"`
* XML `<error>` elements
* `"fault":` fields

## Using with Response Caching

Combine DotHttp testing with cached responses for deterministic tests:

```csharp theme={"dark"}
public class CachedApiTests : DotHttpTestBase
{
    protected override HttpMessageHandler CreateHttpMessageHandler()
    {
        // Serve responses from cached files instead of real API
        return new TestCacheReadDelegatingHandler("ResponseFiles");
    }

    [TestMethod]
    public async Task GetUsers_UseCachedResponse()
    {
        SetVariable("baseUrl", "https://api.example.com");

        var response = await SendRequestAsync(new DotHttpRequest
        {
            Method = "GET",
            Url = "{{baseUrl}}/users"
        });

        // Response comes from ResponseFiles/api.example.com/users.json
        Assert.IsTrue(response.IsSuccessStatusCode);
    }
}
```

See the [Response Caching guide](https://easyaf.nimbleapps.cloud/breakdance/guides/response-caching) for details on capturing and serving cached responses.

## Parsing .http Files

Use `DotHttpFileParser` to parse existing `.http` files:

```csharp theme={"dark"}
[TestMethod]
public async Task ExecuteAllRequestsFromFile()
{
    var parser = new DotHttpFileParser();
    var httpFile = parser.ParseFile("api.http");

    foreach (var request in httpFile.Requests)
    {
        var response = await SendRequestAsync(request);
        await DotHttpAssertions.AssertValidResponseAsync(response);
    }
}
```

## Related Resources

> [card] [Response Caching](guides/web/response-caching)
>
> Capture and replay HTTP responses for deterministic tests

> [card] [In-Memory Web API](guides/web/testing-web-api)
>
> Test ASP.NET Web API controllers without a server


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.