Testing in Bialet#

Bialet includes a built-in testing framework for HTTP endpoint testing. Tests are written in Wren and can verify responses from your application endpoints.

Running Tests#

Use the -T flag to run all tests:

bialet -T                    # Run all tests in _tests/ folder
bialet -T docs/examples      # Run tests in specific directory

Test Directory Structure#

Tests are located in the _tests/ folder within your application:

myapp/
├── _app.wren
├── _migration.wren
├── hi.wren
├── _tests/
   ├── _init.wren           # Optional: runs before test files
   ├── hi.wren              # Tests for hi.wren
   └── password.wren        # Tests for password.wren

Important: The _tests/ folder name starts with _ to prevent direct HTTP access, following Bialet’s convention for private files.

Test Initialization#

_tests/_init.wren (Optional)#

If present, this file runs once before each test file. Use it for common setup:

// _tests/_init.wren
System.log("Setting up test environment")

// Insert test data
`INSERT INTO users (name, email) VALUES (?, ?)`.query("Test User", "test@example.com")

Writing Tests#

Tests use the Test class with a fluent API:

// Basic GET request test
Test.get("/hi").status(200).contains("Hello World")

// POST request test with form data
Test.post("/password", {"password":"123", "password-check":"123"})
    .status(200)
    .contains("The passwords are the same")

Test API Reference#

HTTP Request Methods#

Method

Description

Test.get(route)

Send GET request

Test.post(route, params)

Send POST request with form data

Test.apiGet(route)

Send GET with JSON Content-Type header

Test.apiPost(route, params)

Send POST with JSON body

Test.apiPut(route, params)

Send PUT with JSON body

Test.apiDelete(route)

Send DELETE request

Request Configuration#

Chain these before assertions:

Test.get("/api/user")
    .setHeader("Authorization", "Bearer token123")  // Add custom header
    .method("GET")                                     // Explicitly set method
    .postData({"key": "value"})                       // Set POST data

Assertions#

Chain these after the request method:

Assertion

Description

.status(code)

Assert HTTP status code

.contains(str)

Assert body contains string

.equals(str)

Assert body exactly equals string

.notContains(str)

Assert body doesn’t contain string

.header(name)

Assert header exists

.header(name, value)

Assert header equals value

.json()

Return parsed JSON body (Map or List)

.jsonContains(key)

Assert JSON object has key

.jsonEquals(key, value)

Assert JSON key equals value

.jsonNotContains(key)

Assert JSON object doesn’t have key

.assert(condition, message)

Custom assertion with message

Note: Assertions are lazy - the request is executed when the first assertion is called.

Body Assertions#

// Exact match
Test.get("/health").status(200).equals("OK")

// Contains substring
Test.get("/page").contains("Welcome")

// Negative assertion (security checks)
Test.get("/public").notContains("password")

// Multiple assertions chained
Test.get("/api")
    .status(200)
    .contains("success")
    .notContains("error")

JSON Assertions#

For API endpoints that return JSON:

// Parse JSON body for custom validation
var data = Test.apiGet("/users").status(200).json()
Test.assert(data.count > 0, "Expected at least one user")

// Fluent JSON assertions (from _tests/api-user.wren)
Test.apiGet("/api-user")
    .status(200)
    .jsonContains("id")
    .jsonContains("name")
    .jsonContains("email")
    .jsonEquals("id", 1)
    .jsonEquals("name", "Test User")
    .jsonEquals("active", true)
    .jsonNotContains("password")
    .jsonNotContains("ssn")

Two Approaches to JSON Testing#

Approach 1: Fluent JSON methods (best for simple checks)

// From _tests/api-user.wren
Test.apiGet("/api-user")
    .jsonContains("id")
    .jsonEquals("name", "Test User")
    .jsonNotContains("password")

Approach 2: Parse JSON + custom logic (best for complex validation)

// From _tests/json.wren
var counters = Test.apiGet("/json").json()
Test.assert(counters.count >= 2, "Expected at least 2 counters")

for (counter in counters) {
  if (counter["name"] == "test1") {
    Test.assert(counter["value"] == "10", "Expected value '10'")
  }
}

Multiple Assertions#

Chain multiple assertions on a single request:

Test.get("/api/users")
    .status(200)
    .header("Content-Type", "application/json")
    .contains("user@example.com")

Multiple Tests in One File#

Write multiple independent tests in a single file:

// _tests/api.wren

// Test 1: List users
Test.get("/api/users").status(200).contains("users")

// Test 2: Create user
Test.apiPost("/api/users", {"name": "John"})
    .status(201)
    .contains("John")

// Test 3: Invalid request
Test.post("/api/users", {})
    .status(400)
    .contains("name is required")

API Testing Examples#

JSON Response Testing#

// Test JSON endpoint with json() and assert() (from _tests/json.wren)
// Shows parsing JSON and custom validation logic

// Parse JSON response
var data = Test.apiGet("/json").status(200).json()
Test.assert(data is List, "Expected JSON array")

// Insert test data
`INSERT OR REPLACE INTO counter (name, value) VALUES ('test1', 10)`.query()

// Complex validation with parsed JSON
var counters = Test.apiGet("/json").status(200).json()
Test.assert(counters.count >= 2, "Expected at least 2 counters")

// Iterate and validate specific values
for (counter in counters) {
  if (counter["name"] == "test1") {
    Test.assert(counter["value"] == "10", "Expected value '10'")
  }
}

Multiple Test Patterns#

// From _tests/hi.wren - different assertion styles

// Simple contains check
Test.get("/hi").status(200).contains("Hello World")

// Exact match
Test.get("/hi")
    .status(200)
    .equals("<h1>👋 Hello World</h1>")

// Multiple chained assertions
Test.get("/hi")
    .status(200)
    .contains("Hello")
    .contains("World")
    .notContains("Goodbye")

Security and Negative Assertions#

// From _tests/password.wren - test what should NOT be present
Test.post("/password", {"password": "abc", "password-check": "xyz"})
    .status(200)
    .notContains("The passwords are the same")  // Should not contain success message

// From _tests/assertions.wren
Test.get("/hi")
    .status(200)
    .notContains("Error")
    .notContains("Failed")

Form Testing#

// From _tests/password.wren
Test.post("/password", {"password": "123", "password-check": "123"})
    .status(200)
    .contains("The passwords are the same")

Test.post("/password", {"password": "123", "password-check": "321"})
    .status(200)
    .contains("The passwords are different")

Complete Test Examples#

// From _tests/assertions.wren - comprehensive testing
Test.get("/hi")
    .status(200)
    .equals("<h1>👋 Hello World</h1>")

Test.post("/password", {"password": "test123", "password-check": "test123"})
    .status(200)
    .contains("The passwords are the same")
    .contains("Hash:")

Best Practices#

Use Appropriate Assertion Methods#

// ✅ Good - specific assertions
Test.get("/health").equals("OK")
Test.apiGet("/users").jsonContains("id")

// ❌ Avoid - too generic
Test.get("/health").contains("OK")  // Could match "NOT OK"

Security Testing#

Always check that sensitive data isn’t exposed:

// Ensure passwords/keys not in responses
Test.get("/public/user")
    .notContains("password")
    .jsonNotContains("ssn")
    .jsonNotContains("api_key")

Use Custom Assertions for Complex Logic#

// Instead of manual Fiber.abort()
var data = Test.apiGet("/stats").json()
Test.assert(data["active"] > data["inactive"], "More active expected")
Test.assert(data["total"] == data["active"] + data["inactive"], "Count mismatch")

Test Both Success and Failure Cases#

// Success case
Test.post("/login", {"user": "admin", "pass": "correct"})
    .status(200)
    .contains("Welcome")

// Failure case
Test.post("/login", {"user": "admin", "pass": "wrong"})
    .status(401)
    .contains("Invalid credentials")

Database Isolation#

Tests run against a temporary database file that is created fresh for each test run:

  1. A temporary database file is created

  2. Migrations are automatically run (_migration.wren or /_app/migration.wren)

  3. _tests/_init.wren runs (if exists)

  4. Test files execute

  5. Temporary database is deleted

This ensures tests don’t pollute your development or production database.

Complete Example#

Application file: hi.wren

return <h1>👋 Hello World</h1>

Test file: _tests/hi.wren

Test.get("/hi")
    .status(200)
    .contains("Hello World")

Run tests:

$ bialet -T
Running tests in _tests/...

✓ _tests/hi.wren: Test.get("/hi").status(200).contains("Hello World")

1 passed, 0 failed

Exit Codes#

  • 0: All tests passed

  • 1: One or more tests failed

This makes it easy to integrate with CI/CD pipelines:

# GitHub Actions example
- name: Run tests
  run: bialet -T

How It Works#

  1. Request Building: The Test class builds an HTTP request (method, headers, body)

  2. Internal Routing: Instead of making actual HTTP calls, the test runner directly invokes the Wren handler

  3. Response Capture: The response (status, headers, body) is captured and wrapped

  4. Assertions: Each assertion checks the response and reports failures

  5. Database: All database operations happen on a temp DB, isolated from your main data

This approach is fast (no network overhead), reliable (no port conflicts), and safe (isolated database).