Quality Assurance with rift-verify and rift-lint¶
Validate your mocks before they break your tests
This article is for developers who keep imposter files in a repository and want them checked before a test run depends on them. By the end you will lint those files, verify a running server's stubs, and run both in CI.
You've built complex mock configurations. But how do you know they work correctly? How do you catch issues before they cause test failures?
Rift ships two CLI tools for this, alongside the rift server:
- rift-lint: validates configuration files before they are loaded
- rift-verify: sends a request to every stub of a running server and checks the response
Both are included in the Homebrew formula (brew install achird-labs/rift/rift) and the release
archives. rift-lint also has its own Docker image, zainalpour/rift-lint; rift-verify has none.
rift-lint: Configuration Validation¶
Basic Usage¶
# Lint a single file
rift-lint imposter.json
# Lint a directory
rift-lint ./imposters/
# Strict mode (treat warnings as errors)
rift-lint ./imposters/ --strict
rift-lint reads the same formats --configfile does — JSON (.json) and YAML (.yaml, .yml) —
and scans a directory one level deep, not recursively. It exits non-zero when it finds an error, or
a warning under --strict.
What It Checks¶
Errors are problems Rift refuses or cannot serve correctly — for example a port declared twice
across the directory (E002), a stub with no responses (E006), an unknown predicate operator
(E009), or a header value that is a number or boolean instead of a string (E019, E020).
Warnings flag configurations that load but probably don't do what you meant — for example a stub
that uses state with no _rift.flowState configured (W014). The
linting reference lists every code.
Example Output¶
Two files that share a port, with a typo'd predicate and a numeric header:
Rift Imposter Linter
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Scanning: .
Found: 2 imposter file(s)
FAIL order-service.json (2 error(s))
| [port] error: Port 4545 is used by 2 imposters: order-service.json, user-service.json (E002)
| -> Assign unique ports to each imposter. Consider using ports 4546+
| [stubs[0]] error: Stub missing 'responses' field (E006)
FAIL user-service.json (3 error(s))
| [stubs[0].predicates[0]] error: Unknown predicate operator: eqauls (E009)
| -> Use one of: equals, deepEquals, contains, startsWith, endsWith, matches, exists, not, or, and, inject
| [stubs[0].predicates[0]] error: Predicate has no operator (E008)
| -> Add one of: equals, deepEquals, contains, startsWith, endsWith, matches, exists, not, or, and, inject
| [stubs[0].responses[0].is.headers.X-Count] error: Header 'X-Count' value is a number, must be a string (E019)
| -> Change to: "X-Count": "123"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Files checked: 2
Errors: 5
Warnings: 0
Linting failed with errors
Auto-Fix Mode¶
Some issues can be fixed automatically:
--fix rewrites header values of the wrong type as strings — 123 becomes "123", true becomes
"true" — in JSON files. Everything else it reports but leaves for you to fix.
JSON Output for CI¶
The banner goes to stderr, so stdout is a single JSON document:
{
"issues": [
{
"severity": "error",
"code": "E019",
"message": "Header 'X-Count' value is a number, must be a string",
"file": "./user-service.json",
"location": "stubs[0].responses[0].is.headers.X-Count",
"suggestion": "Change to: \"X-Count\": \"123\""
}
],
"files_checked": 2,
"errors": 5,
"warnings": 0
}
CI/CD Integration¶
# .github/workflows/lint.yml
name: Lint Mock Configs
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint configurations
run: |
docker run --rm -v ${{ github.workspace }}:/imposters \
zainalpour/rift-lint:latest fixtures/ --strict --output json > lint-results.json
- name: Upload results
if: failure()
uses: actions/upload-artifact@v4
with:
name: lint-results
path: lint-results.json
Checking Scripts¶
rift-lint checks a config's structure. To check the scripts inside it, use
rift script check — see Scripting.
rift-verify: Stub Verification¶
What It Does¶
rift-verify:
- Fetches every imposter from a running Rift server's admin API
- Builds a request from each stub's predicates
- Sends it and compares the response with the stub's first response
- Reports mismatches and exits non-zero if any test failed
Basic Usage¶
# Verify every imposter on http://localhost:2525
rift-verify
# A different admin URL
rift-verify --admin-url http://localhost:3525
# One imposter
rift-verify --port 4545
# Print a curl command for each test, and every result rather than only failures
rift-verify --show-curl --verbose
Example Output¶
This server has two imposters. In the Order Service, GET /orders answers 200, and a second stub
catches every other /orders path with 404:
{
"port": 4546,
"protocol": "http",
"name": "Order Service",
"stubs": [
{
"predicates": [{ "equals": { "method": "GET", "path": "/orders" } }],
"responses": [{ "is": { "statusCode": 200, "body": "[]" } }]
},
{
"predicates": [{ "startsWith": { "path": "/orders" } }],
"responses": [{ "is": { "statusCode": 404 } }]
}
]
}
Rift Stub Verifier
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Admin URL: http://localhost:2525
Imposter: User Service (port 4545)
Imposter: Order Service (port 4546)
FAIL Stub #1 - GET /orders
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Verification Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Imposters: 2
Stubs: 6
Tests: 6
Passed: 5
Failed: 1
Skipped: 0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Failure Details
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Imposter :4546 (Order Service) - Stub #1
Request: GET /orders
Expected: status=404, body=None
Actual: status=200, body=Some("[]")
Why it failed:
- Status mismatch: expected 404, got 200
Hint: Expected status 404 but got 200. Verify the stub response configuration.
1 test(s) failed. See details above.
The failure is real, and it is the kind of bug rift-verify exists to find: the request generated
for stub #1 (GET /orders) is caught by stub #0 first, so stub #1 never answers that path. Stubs
match in order.
Dynamic Stubs¶
A stub whose response is computed — inject, proxy, _rift.script, or a stub that cycles
through several responses — has no single expected answer. By default rift-verify tests these
anyway and expects a 200, so a script that deliberately answers 418 is reported as a failure.
Skip them:
Or check only status codes, ignoring bodies and headers:
Dry Run Mode¶
List what would be tested without sending any requests:
Imposter: User Service (port 4545)
DRY-RUN Stub #0 - GET /health
DRY-RUN Stub #1 - GET /users
DRY-RUN Stub #2 - POST /users
DRY-RUN Stub #3 - GET /users/1
Imposter: Order Service (port 4546)
DRY-RUN Stub #0 - GET /orders
DRY-RUN Stub #1 - GET /orders
JSON Output and Timeouts¶
# Machine-readable summary on stdout; progress goes to stderr
rift-verify --output json
# Per-request timeout in seconds (default 10)
rift-verify --timeout 30
rift-verify --help lists the rest, including --gateway for servers reached through the
single-port gateway and --insecure for HTTPS imposters with self-signed certificates.
CI Integration¶
# GitHub Actions
- name: Verify mock stubs
run: |
# Start Rift
docker run -d --name rift -p 2525:2525 -p 4545:4545 \
-v ${{ github.workspace }}/fixtures:/fixtures \
zainalpour/rift-proxy:latest \
--configfile /fixtures/mocks.json
# Wait until the admin API answers
until curl -sf http://localhost:2525/ > /dev/null; do sleep 0.5; done
# Verify (rift-verify must be installed on the runner)
rift-verify --admin-url http://localhost:2525
# Cleanup
docker rm -f rift
rift-tui: Interactive Management¶
Rift also includes an interactive terminal UI for managing a running server's imposters and stubs:
See TUI in the Rift docs for its screens and key bindings.
Combining Tools in Workflows¶
Pre-Commit Hook¶
#!/bin/bash
# .git/hooks/pre-commit
# Lint all imposter files
if ! rift-lint ./fixtures/ --strict; then
echo "Imposter configuration has errors"
exit 1
fi
echo "Imposter configurations are valid"
Full Validation Pipeline¶
#!/bin/bash
# validate-mocks.sh
set -e
echo "Step 1: Linting configurations..."
rift-lint ./fixtures/ --strict
echo "Step 2: Starting Rift..."
rift --configfile ./fixtures/all.json &
RIFT_PID=$!
trap 'kill $RIFT_PID' EXIT
until rift healthcheck 2>/dev/null; do sleep 0.5; done
echo "Step 3: Verifying stubs..."
rift-verify --admin-url http://localhost:2525
echo "Step 4: Running integration tests..."
npm test
echo "All validations passed!"
rift healthcheck exits 0 once the admin API on --port (default 2525) answers, which makes it
a better wait than a fixed sleep.
Best Practices¶
1. Lint Early, Lint Often¶
2. Verify After Changes¶
3. Use Strict Mode in CI¶
4. Keep Verification Fast¶
5. Document Expected Failures¶
Troubleshooting¶
Connection refused¶
Timeouts¶
A dynamic stub reported as failed¶
Conclusion: The Complete Series¶
We've covered everything you need to be productive with Rift:
- Introduction — Why Rift, performance benefits
- Getting Started — First imposters, basic usage
- Migration Guide — Zero-friction Mountebank switch
- Advanced Predicates — JSONPath, XPath, complex matching
- Fault Injection — Chaos engineering made easy
- Flow State — Stateful mock services
- Scripting — Rhai and JavaScript engines
- Proxy Mode — Recording and replaying
- Production Deployment — Docker, K8s, CI/CD
- CLI Tools — rift-verify and rift-lint (this post)
Get Started Today:
# Try Rift
docker run -p 2525:2525 zainalpour/rift-proxy:latest
# Star the repo
# https://github.com/achird-labs/rift
Thank you for following this series! Questions? Feedback? Open an issue on GitHub.
Tags: #CLI #Testing #QualityAssurance #MockServer #Rift #DevTools