Skip to main content
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:
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:
ApiTests.cs
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);
    }
}
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 ###:
api.http
### 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": "[email protected]"
}

### 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:
@baseUrl = https://api.example.com
@apiKey = my-secret-key

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

Setting Variables in Code

[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:
VariableDescriptionExample Output
{{$guid}}New GUID550e8400-e29b-41d4-a716-446655440000
{{$datetime}}UTC datetime (ISO 8601)2024-01-15T10:30:00Z
{{$localDatetime}}Local datetime with timezone2024-01-15T10:30:00-05:00
{{$timestamp}}Unix timestamp1705315800
{{$randomInt}}Random integer42987
{{$randomInt 100}}Random 0-10073
{{$randomInt 10 50}}Random 10-5034
{{$processEnv VAR}}Environment variableValue of $VAR
### 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:
### 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:
http-client.env.json
{
    "$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:
[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:
http-client.env.json.user
{
    "dev": {
        "apiKey": "my-personal-dev-key"
    }
}
LoadEnvironmentWithOverrides(
    "http-client.env.json",
    "http-client.env.json.user",
    "dev");
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:
### 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

ReferenceDescription
{{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

### 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

[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:
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:
// 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:
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 for details on capturing and serving cached responses.

Parsing .http Files

Use DotHttpFileParser to parse existing .http files:
[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);
    }
}