Tutorial: Testing Your Scripts
A complete, step-by-step guide to building a Lua API and testing it with the built-in test runner. Follow along from project setup to a full passing test suite.
What We'll Build
We'll build a Bookmarks API — a simple CRUD service for saving links — and then write a full test suite that exercises every endpoint. By the end you'll have:
2 Lua Scripts
bookmarks.lua (list + create) and bookmarks/_id.lua (get + update + delete)
1 Test File
tests/bookmarks_test.lua with 10 tests covering CRUD, validation, and edge cases
my-project/ ├── solidb-scripts.toml # Project config ├── .env # Auth & server settings ├── .env.test # Test-specific overrides ├── bookmarks.lua # GET, POST /api/default/myapp/bookmarks ├── bookmarks/ │ └── _id.lua # GET, PUT, DELETE /api/default/myapp/bookmarks/:id └── tests/ # Test files (never pushed to server) └── bookmarks_test.lua
1 Initialize the Project
Create a folder and initialize a SoliDB scripts project.
# Create project directory $ mkdir my-project && cd my-project # Initialize with your database name $ solidb scripts init --db myapp # Authenticate (interactive prompt) $ solidb scripts login # Or set an API key directly in .env $ echo 'SOLIDB_API_KEY=your_token_here' > .env
This creates solidb-scripts.toml:
host = "localhost" port = 6745 database = "myapp" service = "default" [scripts] directory = "." tests_dir = "tests" ignore = ["*.bak", ".git", "node_modules", "tests"]
Create a .env.test for test-specific config (optional but recommended):
# Use a separate test database to avoid touching production data
SOLIDB_DATABASE=myapp_test
SOLIDB_API_KEY=your_test_token
tests/ folder is in the ignore list by default. Test files are never pushed to the server — only executed locally.
2 Write the Lua Scripts
We need two scripts: one for listing/creating bookmarks, and one for operating on a single bookmark by ID.
bookmarks.lua — List & Create
-- @methods GET, POST -- @description List or create bookmarks -- @collection bookmarks local bookmarks = db:collection("bookmarks") if request.method == "GET" then local tag = request.query.tag local results if tag then results = db:query( "FOR b IN bookmarks FILTER @tag IN b.tags SORT b.created_at DESC RETURN b", { tag = tag } ) else results = db:query( "FOR b IN bookmarks SORT b.created_at DESC RETURN b" ) end return { bookmarks = results, count = #results } end if request.method == "POST" then local body = request.body -- Validate if not body.url or body.url == "" then solidb.error("url is required", 400) end if not body.title or body.title == "" then solidb.error("title is required", 400) end local bookmark = bookmarks:insert({ url = body.url, title = body.title, tags = body.tags or {}, notes = body.notes or "", created_at = time.now() }) solidb.status(201) return { bookmark = bookmark } end
bookmarks/_id.lua — Get, Update & Delete
-- @methods GET, PUT, DELETE -- @description Get, update or delete a bookmark -- @collection bookmarks local bookmarks = db:collection("bookmarks") local id = request.params.id local bookmark = bookmarks:get(id) if not bookmark then solidb.error("Bookmark not found", 404) end if request.method == "GET" then return bookmark end if request.method == "PUT" then local body = request.body local updates = { updated_at = time.now() } if body.url then updates.url = body.url end if body.title then updates.title = body.title end if body.tags then updates.tags = body.tags end if body.notes then updates.notes = body.notes end local updated = bookmarks:update(id, updates) return { bookmark = updated } end if request.method == "DELETE" then bookmarks:delete(id) return { message = "Bookmark deleted", id = id } end
3 Deploy to the Server
Push your scripts so the test runner can make real HTTP requests against them.
$ solidb scripts push Pushing 2 file(s) to http://localhost:6745... ✓ Created: bookmarks.lua -> bookmarks [GET, POST] ✓ Created: bookmarks/_id.lua -> bookmarks/:id [GET, PUT, DELETE] Done: 2 created, 0 updated, 0 errors
.env.test to keep your production data safe.
4 Write the Tests
Create tests/bookmarks_test.lua. This is the complete, working test file.
--------------------------------------------------------------- -- Bookmarks API Test Suite -- -- Run with: solidb scripts test -- Filter: solidb scripts test --filter "create" --------------------------------------------------------------- -- Shared state across tests local created_id = nil -- ============================================================ -- CREATE -- ============================================================ describe("POST /bookmarks", function() it("creates a bookmark with valid data", function() local res = http.post("/bookmarks", { url = "https://solidb.io", title = "SoliDB Homepage", tags = { "database", "rust" }, notes = "The best database" }) expect(res.status):to_equal(201) expect(res.body.bookmark):to_exist() expect(res.body.bookmark._key):to_exist() expect(res.body.bookmark.url):to_equal("https://solidb.io") expect(res.body.bookmark.title):to_equal("SoliDB Homepage") -- Save for later tests created_id = res.body.bookmark._key end) it("rejects a bookmark without a url", function() local res = http.post("/bookmarks", { title = "Missing URL" }) expect(res.status):to_equal(400) expect(res.body.error):to_contain("url") end) it("rejects a bookmark without a title", function() local res = http.post("/bookmarks", { url = "https://example.com" }) expect(res.status):to_equal(400) expect(res.body.error):to_contain("title") end) it("defaults tags to empty array and notes to empty string", function() local res = http.post("/bookmarks", { url = "https://example.com/defaults", title = "Defaults Test" }) expect(res.status):to_equal(201) expect(res.body.bookmark.notes):to_equal("") -- Clean up http.delete("/bookmarks/" .. res.body.bookmark._key) end) end) -- ============================================================ -- READ -- ============================================================ describe("GET /bookmarks", function() it("lists all bookmarks", function() local res = http.get("/bookmarks") expect(res.status):to_equal(200) expect(res.body.bookmarks):to_exist() expect(res.body.count):to_be_greater_than(0) end) it("filters bookmarks by tag", function() local res = http.get("/bookmarks?tag=rust") expect(res.status):to_equal(200) expect(res.body.bookmarks):to_exist() expect(res.body.count):to_be_greater_than(0) end) it("gets a single bookmark by ID", function() local res = http.get("/bookmarks/" .. created_id) expect(res.status):to_equal(200) expect(res.body.url):to_equal("https://solidb.io") expect(res.body.title):to_equal("SoliDB Homepage") end) end) -- ============================================================ -- UPDATE -- ============================================================ describe("PUT /bookmarks/:id", function() it("updates a bookmark's title", function() local res = http.put("/bookmarks/" .. created_id, { title = "SoliDB - Multi-Document Database" }) expect(res.status):to_equal(200) expect(res.body.bookmark.title):to_equal("SoliDB - Multi-Document Database") -- Verify the change persisted local check = http.get("/bookmarks/" .. created_id) expect(check.body.title):to_equal("SoliDB - Multi-Document Database") end) end) -- ============================================================ -- DELETE -- ============================================================ describe("DELETE /bookmarks/:id", function() it("deletes a bookmark", function() local res = http.delete("/bookmarks/" .. created_id) expect(res.status):to_equal(200) expect(res.body.message):to_contain("deleted") end) it("returns 404 for a deleted bookmark", function() local res = http.get("/bookmarks/" .. created_id) expect(res.status):to_equal(404) end) end)
expect(val):to_equal(42), not dot. This is Lua's method-call syntax that passes the expectation object as the first argument.
5 Run the Tests
$ solidb scripts test • Using API key from SOLIDB_API_KEY environment variable • Testing against http://localhost:6745 (database: myapp_test, service: default) Running tests from /home/user/my-project... POST /bookmarks ✓ creates a bookmark with valid data (42ms) ✓ rejects a bookmark without a url (12ms) ✓ rejects a bookmark without a title (11ms) ✓ defaults tags to empty array and notes to empty string (38ms) GET /bookmarks ✓ lists all bookmarks (18ms) ✓ filters bookmarks by tag (22ms) ✓ gets a single bookmark by ID (15ms) PUT /bookmarks/:id ✓ updates a bookmark's title (31ms) DELETE /bookmarks/:id ✓ deletes a bookmark (19ms) ✓ returns 404 for a deleted bookmark (10ms) ────────────────────────────────────────────────── Tests: 10 passed Time: 218ms
CLI Options
When a Test Fails
POST /bookmarks ✓ creates a bookmark with valid data (42ms) ✗ rejects a bookmark without a url (15ms) Expected 200 to equal 400 ────────────────────────────────────────────────── Tests: 1 passed, 1 failed Time: 57ms
The exit code is 1 when any test fails, so you can use it in CI pipelines.
Reference: HTTP Client
The http module is pre-configured with your server URL, database, service, and auth token. Paths are relative to your service endpoint.
| Function | Example |
|---|---|
| http.get(path, headers?) | http.get("/bookmarks?tag=rust") |
| http.post(path, body?, headers?) | http.post("/bookmarks", { url = "..." }) |
| http.put(path, body?, headers?) | http.put("/bookmarks/abc", { title = "New" }) |
| http.patch(path, body?, headers?) | http.patch("/bookmarks/abc", { tags = {} }) |
| http.delete(path, headers?) | http.delete("/bookmarks/abc") |
Response Object
{
status = 200, -- HTTP status code (number)
ok = true, -- true if status is 2xx
body = { ... }, -- JSON body parsed as Lua table
headers = { -- Response headers
["content-type"] = "application/json"
}
}
Custom Headers
-- Pass custom headers as the last argument local res = http.get("/bookmarks", { ["X-Request-ID"] = "test-123", ["Accept-Language"] = "fr" })
Reference: Assertions
Use expect(value) followed by a colon matcher.
| Matcher | Description |
|---|---|
| :to_equal(expected) | Deep equality (works on tables, strings, numbers, booleans) |
| :to_exist() | Value is not nil |
| :to_be_nil() | Value is nil |
| :to_be_true() / :to_be_false() | Boolean checks |
| :to_contain(substring) | String contains substring |
| :to_match(regex) | String matches regex pattern |
| :to_be_greater_than(n) | Number > n |
| :to_be_less_than(n) | Number < n |
| :to_throw() | Function raises an error when called |
Examples
-- Equality expect(res.status):to_equal(200) expect(res.body.name):to_equal("Alice") -- Existence expect(res.body.token):to_exist() expect(res.body.deleted_field):to_be_nil() -- Strings expect(res.body.error):to_contain("required") expect(res.body.id):to_match("^[a-f0-9]+") -- Numbers expect(res.body.count):to_be_greater_than(0) expect(res.body.latency_ms):to_be_less_than(1000) -- Booleans expect(res.ok):to_be_true() expect(res.body.is_admin):to_be_false() -- Errors expect(function() error("boom") end):to_throw()
Reference: Test Hooks
Setup and teardown functions for managing test data.
describe("With Hooks", function() -- Runs once before any test in this describe block before_all(function() http.post("/bookmarks", { url = "https://seed.example.com", title = "Seed Data" }) end) -- Runs before each it() block before(function() print("starting test...") -- visible with --verbose end) -- Runs after each it() block after(function() print("done") end) -- Runs once after all tests in this describe block after_all(function() -- clean up seed data here end) it("does something", function() -- ... end) end)
Reference: Utilities
Additional modules available in test files.
| Function | Description |
|---|---|
| json.encode(table) | Serialize Lua table to JSON string |
| json.decode(string) | Parse JSON string into Lua table |
| print(...) | Output debug info (visible with --verbose) |
Tips
Use a Test Database
Set SOLIDB_DATABASE=myapp_test in .env.test. The test runner loads this file automatically.
Clean Up After Yourself
Delete resources you create, either at the end of each test or in after_all. This prevents test pollution.
Use Watch + Test Together
Run solidb scripts watch in one terminal to auto-deploy on save, and solidb scripts test in another after each change.
Run in CI/CD
solidb scripts test exits with code 1 on failure. Add it to your pipeline after solidb scripts push.
Filter When Debugging
Use --filter "DELETE" to run only the describe blocks that match, so you can iterate fast on a single failing test.