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.