← Back to list

API Tests Should Read Like Specifications — Building LashTest

Most API tests start simple.

Sid-Ali Hamdane · 2026-05-18 08:25 · 1 claps · 4.3 min read
#python #api-testing #qa #pytest #test-automation
Open on Medium ↗

API Tests Should Read Like Specifications — Building LashTest

Most API tests start simple.

Then they slowly become a wall of:

  • setup code
  • repetitive assertions
  • auth boilerplate
  • retry utilities
  • reporting glue
  • unreadable request handling

At some point, the tests become harder to maintain than the API itself.

I kept running into this problem across backend projects, especially when integration test suites started growing. The tests technically worked, but they didn’t feel good to write or read.

I wanted API tests to feel more like readable specifications than infrastructure code.

So I built LashTest — a Python library for expressive API testing with built-in Allure reporting.

The Problem With Many API Tests

A lot of API testing setups eventually drift into something like this:

response = requests.post(
    "https://api.example.com/v1/users",
    headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    },
    json={
        "name": "Alice",
        "email": "alice@example.com"
    },
    timeout=10
)

assert response.status_code == 201
body = response.json()
assert "id" in body
assert body["name"] == "Alice"
assert response.elapsed.total_seconds() < 1.5

There’s nothing inherently wrong with this approach.

But as test suites grow, the same patterns repeat everywhere:

  • authentication handling
  • retries
  • response validation
  • schema checks
  • reporting integration
  • XML parsing
  • fake data generation

Eventually, every project starts building its own testing layer on top of existing tools.

LashTest was designed to make those workflows feel native instead of bolted on.

What I Wanted From an API Testing Library

I designed LashTest around a few ideas:

  • API tests should be readable at a glance
  • Common workflows should require minimal setup
  • Reporting should happen automatically
  • XML APIs should not feel painful
  • Authentication and retries should be built in
  • Tests should scale without turning into infrastructure code

The goal was not to replace pytest or reinvent HTTP clients.

The goal was to create a focused developer experience for API testing.

A Simple Example

Here’s a basic test using LashTest:

from lashtest import APIClient
client = APIClient('https://jsonplaceholder.typicode.com')

user = {
  "username" : "Johndoe",
  "password" : "test1234",
  "email": "test@mail.com",
}

def test_get_user():
    with client.get('/users/1') as response:
        response.assert_status(200) \
                .assert_json_contains({'id': 1}) \
                .assert_response_time(2.0)

def test_post_user():
    with client.post('/users').with_body() as response:
        response.assert_status(200) \
                .assert_json_contains('id') \
                .assert_json_contains({'username':'Johndoe'})
                .assert_response_time(2.0)

The test reads almost like a specification:

  • send request
  • verify status
  • validate response body
  • check performance

No extra setup. No custom wrappers. No repetitive assertion utilities.

Fluent Request Building

One of the main goals was reducing noisy setup code.

LashTest uses a fluent builder API so requests can be composed naturally.

with (
  client.post('/users')
    .with_json({
        'name': 'Alice',
        'email': 'alice@example.com',
    })
    .with_retry(max_attempts=3)
) as response:
    response.assert_status(201)

All request configuration methods are chainable:

  • Headers
  • Query parameters
  • JSON bodies
  • Auth
  • Retries
  • File uploads
  • Timeouts
  • Ssl config

This keeps tests compact while remaining explicit.

Built-In Assertions

API tests often end up with huge assertion blocks.

LashTest includes built-in assertions for the workflows that appear constantly in integration testing.

response \
    .assert_ok() \
    .assert_json_path_exists('$.id') \
    .assert_json_path('$.name', 'Alice') \
    .assert_response_time(1.5)

The library supports:

  • status assertions
  • JSON body assertions
  • JSONPath validation
  • JSON Schema validation
  • headers and cookies
  • performance assertions

This removes a lot of repetitive helper code from test suites.

XML APIs Deserve Better Tooling

One thing I noticed while working on API testing is how awkward XML testing often feels.

Many tools handle JSON well, but XML workflows still feel like second-class citizens.

LashTest includes XPath assertions with automatic namespace detection.

response.assertions.xml.xpath('//soap:Body').exists()

It works with:

  • SOAP
  • RSS
  • Atom feeds
  • SVG
  • default namespaces

You can also perform richer assertions:

response.assertions.xml.xpath('//book').count.gte(5)
response.assertions.xml.xpath('//item').first.text.eq('First Item')

The namespace handling is automatic, which removes a surprising amount of friction from XML testing.

Authentication Without Boilerplate

Authentication is another thing that gets rewritten constantly.

LashTest supports:

  • Bearer tokens
  • Basic auth
  • API keys

Example:

from lashtest.http import BearerToken

client = (
    APIClient('https://api.example.com')
    .with_auth(BearerToken('my-token'))
)

You can also override authentication per request:

with client.get('/admin').with_auth(BearerToken('admin-token')) as response:
    response.assert_ok()

Retry Logic Built In

Retries are common in integration testing, especially with:

  • flaky environments
  • async systems
  • CI pipelines
  • eventually consistent APIs

Instead of implementing retry decorators repeatedly, LashTest includes configurable retry support directly in the request builder.

with (
    client.post('/submit')
    .with_json({'data': 'value'})
    .with_retry(max_attempts=3, on_status=[500, 502, 503, 504])
) as response:
  response.assert_ok()

Retries use exponential backoff automatically: 1 second → 2 seconds → 4 seconds

Built-In Allure Reporting

One feature I especially wanted was automatic reporting integration.

LashTest automatically records:

  • requests
  • responses
  • payloads
  • response bodies

as Allure report steps.

Running tests:

lashtest run tests/ --allure-dir allure-results

Generating reports:

lashtest report

Or directly with the Allure CLI:

allure serve allure-results

The goal was to eliminate the extra reporting glue code that often appears in API test frameworks.

Decorators for Cleaner Test Metadata

LashTest also includes lightweight decorators for organizing reports and test metadata.

from lashtest.decorators import title, severity, tag

@title("POST /users creates a user")
@severity('critical')
@tag('users', 'smoke')
def test_create_user(client):
    ...

These integrate naturally with Allure reporting and test filtering.

A More Complete Example

Here’s a more realistic example combining several features together:

from lashtest.decorators import title, severity
from lashtest.utils import fake

@title("POST /users creates a user")
@severity('critical')
def test_create_user(client):
    with (
        client.post('/users')
        .with_json({
            'name': fake.name(),
            'email': fake.email(),
        })
        .with_retry(max_attempts=3)
    ) as response:
        response \
            .assert_status(201) \
            .assert_json_path_exists('$.id') \
            .assert_response_time(1.5)

This is the style of testing experience I wanted:

  • readable
  • concise
  • expressive
  • production-oriented

Still Early — And Moving Fast

LashTest is still in its early development phase.

The API is evolving quickly, features are being added rapidly, and some parts of the library will likely change as real-world usage grows.

Right now, the most valuable thing is feedback from developers actually using it in real projects.

I’m especially interested in:

  • API testing workflows people struggle with
  • missing assertion patterns
  • CI/CD integration pain points
  • reporting improvements
  • XML/SOAP edge cases
  • ergonomics and readability feedback

Contributions, issues, feature requests, and even criticism are all genuinely helpful at this stage.

The goal is to shape the library around practical testing workflows rather than building features in isolation.

What LashTest Is — And Isn’t

LashTest is intentionally focused.

It’s not trying to become:

  • a full testing framework
  • a replacement for pytest
  • an ORM
  • a browser automation tool

It focuses specifically on API and integration testing workflows.

The library tries to reduce friction around:

  • request building
  • assertions
  • retries
  • authentication
  • reporting
  • XML handling

while keeping tests readable as suites grow.

Installation

pip install lashtest

Run tests:

lashtest run tests/

Generate reports:

lashtest report

Final Thoughts

Building LashTest taught me something interesting:

Most API testing pain doesn’t come from HTTP requests themselves.

It comes from everything surrounding them:

  • setup
  • readability
  • assertions
  • retries
  • reporting
  • maintainability

I wanted a library that handled those workflows cleanly without requiring massive configuration or custom infrastructure.

LashTest is my attempt at that.

If you work heavily with REST APIs, SOAP services, or integration testing in Python, I’d love feedback from backend engineers, QA developers, and test automation teams.

The project is still evolving quickly, and community feedback will directly shape where it goes next.

GitHub: https://github.com/sidalihmdn/lashtest

PyPI: https://pypi.org/project/lashtest/


메타데이터
post_id
795d4d50ea6b
slug
api-tests-should-read-like-specifications-building-lashtest-795d4d50ea6b
url
https://medium.com/@eio.hmdn/api-tests-should-read-like-specifications-building-lashtest-795d4d50ea6b
canonical_url
https://medium.com/@eio.hmdn/api-tests-should-read-like-specifications-building-lashtest-795d4d50ea6b
author_url
https://medium.com/@eio.hmdn
status
ok
fetched_at
2026-06-09 15:37:30