ESC
Type to search...
S
Soli Docs

Testing

Soli provides a comprehensive testing framework for MVC applications with BDD-style DSL, parallel execution, and coverage reporting.

Test Structure

Tests live in the tests/ directory of your application:

Directory Layout
myapp/
├── app/
│   ├── controllers/
│   ├── models/
│   └── views/
└── tests/
    ├── users_spec.soli
    ├── posts_spec.soli
    └── integration/
        └── api_spec.soli

Test DSL

Basic Test Structure

tests/users_spec.soli
describe("UsersController", fn() {
    test("creates a new user", fn() {
        # Test code here
        expect(true).to_be(true);
    });

    context("when valid", fn() {
        test("returns success", fn() {
            # Nested context
        });
    });
});

Available Functions

describe(name, fn)

Group related tests

context(name, fn)

Group tests with conditions

test(name, fn)

Define a test case

before_each(fn)

Setup before each test

after_each(fn)

Teardown after each test

pending()

Skip a test

Expectations

Assertions
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_be_greater_than(10);
expect(value).to_be_less_than(100);
expect(value).to_contain("substring");
expect(value).to_match(regex);
expect(hash).to_have_key("name");
expect(json_string).to_be_valid_json();

HTTP Integration Testing

Make HTTP requests to test your endpoints:

tests/api_spec.soli
describe("Users API", fn() {
    test("GET /users returns list", fn() {
        let response = TestHTTP.get("/users");
        expect(response.status).to_equal(200);
        expect(response.body).to_contain("users");
    });

    test("POST /users creates user", fn() {
        let response = TestHTTP.post("/users", hash(
            "email": "[email protected]",
            "name": "Test User"
        ));
        expect(response.status).to_equal(201);
    });

    test("PUT /users/:id updates user", fn() {
        let response = TestHTTP.put("/users/1", hash("name": "Updated"));
        expect(response.status).to_equal(200);
    });

    test("DELETE /users/:id removes user", fn() {
        let response = TestHTTP.delete("/users/1");
        expect(response.status).to_equal(204);
    });
});

Controller Testing

Call controller actions directly with mocked context:

tests/controllers_spec.soli
describe("UsersController", fn() {
    before_each(fn() {
        Factory.clear();
    });

    test("create action", fn() {
        let result = ControllerTest.helpers.users_controller.create(
            params: hash("email": "[email protected]"),
            session: Session.new(),
            headers: Headers.new()
        );
        expect(result.status).to_equal(201);
    });

    test("show action", fn() {
        let user = Factory.create("user");
        let result = ControllerTest.helpers.users_controller.show(
            params: hash("id": user.id),
            session: Session.new(),
            headers: Headers.new()
        );
        expect(result.status).to_equal(200);
    });
});

Database Testing

Transaction Rollback

Tests are isolated using database transactions:

tests/models_spec.soli
describe("User model", fn() {
    test("creates user", fn() {
        with_transaction(fn() {
            let user = Factory.create("user", hash("name": "Test"));
            expect(User.count()).to_equal(1);
            expect(user.name).to_equal("Test");
        });
        # Transaction automatically rolls back
    });
});

Factory Pattern

Using Factories
# 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:

CLI Options
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 Output
Coverage: 87.5% (1250/1428 lines) ✓

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

Generate coverage report

--coverage=html

HTML report

--coverage=json

JSON for CI

--coverage-min=80

Fail if < 80%

Complete Example

tests/users_spec.soli
describe("UsersController", fn() {
    before_each(fn() {
        Factory.clear();
        Database.clean_all();
    });

    context("POST /users", fn() {
        test("creates user with valid data", fn() {
            let response = TestHTTP.post("/users", hash(
                "email": "[email protected]",
                "name": "Test User"
            ));
            expect(response.status).to_equal(201);
            expect(response.body).to_contain("Test User");
        });

        test("returns 422 with invalid email", fn() {
            let response = TestHTTP.post("/users", hash(
                "email": "invalid-email"
            ));
            expect(response.status).to_equal(422);
        });
    });

    context("GET /users/:id", fn() {
        test("shows user profile", fn() {
            let user = Factory.create("user");
            let response = TestHTTP.get("/users/" + user.id);
            expect(response.status).to_equal(200);
            expect(response.body).to_contain(user.name);
        });

        test("returns 404 for unknown user", fn() {
            let response = TestHTTP.get("/users/99999");
            expect(response.status).to_equal(404);
        });
    });
});

Best Practice

Write tests as you develop. Following TDD (Test-Driven Development) helps catch bugs early and improves code design.

Next Steps