ESC
Type to search...
S
Soli Docs

Testing

Comprehensive testing guide for Soli MVC applications. Includes unit testing and end-to-end controller testing.

E2E Controller Testing

Rails-like end-to-end testing framework for Soli MVC applications. Test your controllers with real HTTP requests.

The E2E testing framework provides a comprehensive set of helpers for testing your Soli controllers with real HTTP requests. Built on a test server that runs alongside your test suite, it enables you to write integration tests that simulate actual browser requests and verify controller responses, sessions, and view data.

This framework follows conventions inspired by RSpec Rails testing patterns, making it familiar to developers coming from Ruby on Rails backgrounds while providing the safety and expressiveness of Soli's type system.

Basic Test Structure

Every E2E test file follows the same structure using Soli's test DSL. The framework provides functions for grouping tests, setting up test data, making HTTP requests, and asserting expected outcomes.

describe("HomeController", def() {
    test("GET /up returns UP status", def() {
        let response = get("/up");
        assert_eq(res_status(response), 200);
        assert_eq(res_body(response), "UP");
    });
});

Running Tests

Execute your E2E tests using the Soli test runner:

soli test tests/builtins/controller_integration_spec.sl
soli test tests/builtins

Request Helpers

Request helpers enable you to make HTTP requests to your controllers from within tests. These functions interact with the test server running on a random available port.

HTTP Method Functions

get(path)

GET request without modifying server state

post(path, data)

POST with body to create resources

put(path, data)

PUT replacement of existing resources

patch(path, data)

PATCH partial updates

delete(path)

DELETE resources

head(path)

HEAD request without body

let response = get("/posts");
assert_eq(res_status(response), 200);
let posts = res_json(response);
assert_gt(len(posts), 0);

let response = post("/posts", {
    "title": "New Post",
    "content": "Hello World"
});
assert_eq(res_status(response), 201);

Custom Headers

Add custom headers to your requests:

set_header("X-Request-ID", "test-123");
set_header("X-Custom-Header", "custom-value");

let response = get("/api/data");
clear_headers();

Authentication Headers

with_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...");
let response = get("/api/protected");
clear_authorization();

Cookie Management

set_cookie("session_id", "abc123session");
let response = get("/dashboard");
clear_cookies();

Response Helpers

Response helpers inspect HTTP responses returned by your controllers.

Status Codes

res_status(response)

Returns HTTP status code as integer

res_ok(response)

Checks for 2xx status codes

res_client_error(response)

Checks for 4xx status codes

res_server_error(response)

Checks for 5xx status codes

res_not_found(response)

Checks for 404 status

res_unauthorized(response)

Checks for 401 status

Response Body

let body = res_body(response);
assert_contains(body, "expected text");

let response = post("/users", {"name": "John"});
let user = res_json(response);
assert_eq(user["name"], "John");

Response Headers

let content_type = res_header(response, "Content-Type");
assert_contains(content_type, "application/json");

let headers = res_headers(response);
assert_hash_has_key(headers, "Content-Type");

Redirects

assert(res_redirect(response));

let location = res_location(response);
assert_eq(location, "/expected/path");

Session Helpers

Session helpers manage authentication state and session data during tests.

Authentication State

as_guest()

Clears all authentication state

as_user(user_id)

Simulates logged-in user

as_admin()

Simulates authenticated admin

login(email, password)

Performs login request

Session Inspection

signed_in()

Returns true if authenticated

signed_out()

Returns true if not authenticated

current_user()

Returns authenticated user data

logout()

Destroys current session

Assigns Helpers

Assigns helpers inspect data passed to views during template rendering.

assigns()

Returns all assigns as a hash

assign(key)

Retrieves specific assign value

view_path()

Returns rendered template path

flash()

Returns flash messages

Complete Example

describe("PostsController", def() {
    before_each(def() {
        as_guest();
    });

    test("creates post with valid data", def() {
        login("[email protected]", "password123");
        
        let response = post("/posts", {
            "title": "New Post Title",
            "body": "Post content here"
        });
        
        assert_eq(res_status(response), 201);
        let result = res_json(response);
        assert_not_null(result["id"]);
    });

    test("rejects unauthenticated request", def() {
        let response = post("/posts", {"title": "Test"});
        assert_eq(res_status(response), 302);
    });

    test("shows single post", def() {
        let response = get("/posts/1");
        assert_eq(res_status(response), 200);
        let post = res_json(response);
        assert_eq(post["title"], "First Post");
    });
});

Best Practices

Test Organization

Structure your tests hierarchically using describe() blocks. Group tests by controller, then by action, then by concern.

Before and After Hooks

Use before_each() and after_each() to set up and clean up test state. Always reset authentication state between tests.

Test Isolation

Each test should be independent and not rely on the state created by other tests.

Test DSL

Soli's testing framework provides a BDD-style DSL for organizing and writing tests:

Available Functions

describe(name, def)

Group related tests

context(name, def)

Group tests with conditions

test(name, def)

Define a test case

before_each(def)

Setup before each test

after_each(def)

Teardown after each test

pending()

Skip a test

Assertions

Soli provides assertion functions for writing tests with expressive syntax:

assert_equal(expected, actual, message)

Asserts that two values are equal.

assert_equal(42, result, "should return 42");
assert_equal("hello", str, "string should match");
assert_true(value, message)

Asserts that a value is true.

assert_true(user.is_active, "user should be active");
assert_false(value, message)

Asserts that a value is false.

assert_false(user.is_blocked, "user should not be blocked");
assert_contains(haystack, needle, message)

Asserts that a collection contains a specific value.

assert_contains(users, "admin", "should contain admin user");
assert_nil(value, message)

Asserts that a value is nil (null).

assert_nil(result.error, "should have no error");
assert_not_nil(value, message)

Asserts that a value is not nil.

assert_not_nil(user.id, "user should have an id");

Assertion Result

All assertion functions return a result hash:

{
    "passed": true,
    "message": "test description",
    "expected": value_that_was_expected,
    "actual": value_that_was_actual
}

Expect Syntax (Alternative)

expect(value).to_equal(expected);
expect(value).to_be(expected);
expect(value).to_not_equal(other);
expect(value).to_be_null();
expect(value).to_not_be_null();
expect(value).to_contain("substring");

Factory Functions

Factory.define(name, data)

Define a factory with default data.

Factory.define("user", {
    "name": "Test User",
    "email": "[email protected]"
})
Factory.create(name)

Create an instance from a factory.

let user = Factory.create("user")
Factory.create_with(name, overrides)

Create an instance with custom overrides.

let admin = Factory.create_with("user", { "role": "admin" })
Factory.create_list(name, count)

Create multiple instances

Factory.sequence(name)

Get auto-incrementing number

Factory.clear()

Clear all factories

Database Testing

Transaction Rollback

Tests are isolated using database transactions:

describe("User model", def() {
    test("creates user", def() {
        with_transaction(def() {
            let user = Factory.create("user", hash("name": "Test"));
            expect(User.count()).to_equal(1);
            expect(user.name).to_equal("Test");
        });
    });
});

Factory Pattern

# Define factories
Factory.define("user", hash(
    "email": "[email protected]",
    "name": "Test User"
));

Factory.define("post", hash(
    "title": "Test Post",
    "content": "Content here"
));

# Use factories
let user = Factory.create("user");
let post = Factory.create("post", hash("title": "Custom Title"));
let users = Factory.create_list("user", 5);

Parallel Execution

Tests run in parallel by default:

soli test                    # Parallel (default)
soli test --jobs=4           # 4 workers
soli test --jobs=1           # Sequential (debug)

Coverage Reporting

Generate coverage reports for your tests:

Coverage: 87.5% (1250/1428 lines) ✓

src/controllers/users.sl     ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░  94.2%
src/models/user.sl           ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░  91.1%
src/controllers/posts.sl     ▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░  78.5%
--coverage

Generate coverage report

--coverage=html

HTML report

--coverage=json

JSON for CI

--coverage-min=80

Fail if < 80%