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:

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):

.env.test
# Use a separate test database to avoid touching production data
SOLIDB_DATABASE=myapp_test
SOLIDB_API_KEY=your_test_token
Tip: The 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

bookmarks.lua GET, POST /api/default/myapp/bookmarks
-- @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

bookmarks/_id.lua GET, PUT, DELETE /api/default/myapp/bookmarks/:id
-- @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
Important: Tests run against a live server. They create and delete real data. Use a dedicated test database via .env.test to keep your production data safe.

4 Write the Tests

Create tests/bookmarks_test.lua. This is the complete, working test file.

tests/bookmarks_test.lua 10 tests
---------------------------------------------------------------
-- 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)
Syntax note: Assertions use the colon operator — 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

# Run a single test file
$ solidb scripts test bookmarks_test.lua
# Filter by describe/it name
$ solidb scripts test --filter "DELETE"
# Verbose output (show print() calls inside tests)
$ solidb scripts test --verbose

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

1.

Use a Test Database

Set SOLIDB_DATABASE=myapp_test in .env.test. The test runner loads this file automatically.

2.

Clean Up After Yourself

Delete resources you create, either at the end of each test or in after_all. This prevents test pollution.

3.

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.

4.

Run in CI/CD

solidb scripts test exits with code 1 on failure. Add it to your pipeline after solidb scripts push.

5.

Filter When Debugging

Use --filter "DELETE" to run only the describe blocks that match, so you can iterate fast on a single failing test.