API Tests Should Read Like Specifications — Building LashTest
Most API tests start simple.
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.
메타데이터
- 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