Skip to main content

Testing API Reference

Testing API Reference

The Turn.io SDK provides comprehensive testing utilities through the turn.test.* module. These functions help you write thorough tests for your apps with minimal boilerplate.

State Management

turn.test.reset()

Resets all test state and pre-populates test data. Call this in your before() hooks to start each test with a clean slate.

What it does:

  • Clears all HTTP and app call mocks
  • Resets configuration to defaults
  • Creates 2 test contacts with predictable properties
  • Creates 2 test journeys for testing
  • Clears all lease data
describe("My tests", function()
before(function()
turn.test.reset() -- Clean slate before each test
end)

it("should use pre-populated test data", function()
local contacts = turn.test.get_contacts()
assert(#contacts == 2) -- contact-1 and contact-2
end)
end)

Pre-populated Test Data:

After calling reset(), you automatically have:

  • Contact 1: {uuid="contact-1", name="Test Contact 1", wa_id="27123456789"}
  • Contact 2: {uuid="contact-2", name="Test Contact 2", wa_id="27987654321"}
  • Journey 1: {uuid="journey-1", name="Test Journey 1"}
  • Journey 2: {uuid="journey-2", name="Test Journey 2"}

Configuration Testing

turn.test.set_config(key, value)

Set configuration values for your tests. Useful for testing different config scenarios.

turn.test.set_config("api_key", "test-key-123")
turn.test.set_config("timeout", 30)

local api_key = turn.configuration.get("api_key")
assert(api_key == "test-key-123")

turn.test.get_config(key)

Retrieve configuration values set in tests.

local timeout = turn.test.get_config("timeout")
assert(timeout == 30)

HTTP Request Mocking

turn.test.mock_http(expected_request, mock_response)

Mock HTTP requests to external APIs. Essential for testing API integrations without making real network calls.

Parameters:

  • expected_request - Table with method and url (optional: body, headers)
  • mock_response - Table with status, body, and optional headers
-- Mock a successful API call
turn.test.mock_http(
{
method = "POST",
url = "https://api.example.com/users"
},
{
status = 200,
headers = {["content-type"] = "application/json"},
body = json.encode({success = true, user_id = "123"})
}
)

-- Now HTTP requests will return the mock response
local response = turn.http.request({
method = "POST",
url = "https://api.example.com/users",
body = json.encode({name = "John"})
})

assert(response.status == 200)

turn.test.assert_http_called(expected_request)

Verify that an HTTP request was made with specific parameters.

-- Make a request
turn.http.request({
method = "GET",
url = "https://api.example.com/data"
})

-- Verify it was called
turn.test.assert_http_called({
method = "GET",
url = "https://api.example.com/data"
})

turn.test.get_http_requests()

Get all HTTP requests made during the test. Useful for detailed verification.

-- Make some requests
turn.http.request({method = "POST", url = "https://api.com/users"})
turn.http.request({method = "GET", url = "https://api.com/users/123"})

-- Get all requests
local requests = turn.test.get_http_requests()
assert(#requests == 2, "Expected 2 HTTP requests")
assert(requests[1].method == "POST")
assert(requests[2].method == "GET")

App-to-App Call Mocking

turn.test.mock_app_call(app_name, event_name, mock_response)

Mock calls to other apps in your system. Essential for testing multi-app integrations.

Parameters:

  • app_name - Name of the app being called
  • event_name - Event/action name
  • mock_response - Response data to return
-- Mock a payment app call
turn.test.mock_app_call("payment_app", "process_payment", {
success = true,
transaction_id = "txn_123",
amount = 100
})

-- Now app calls will return the mock
local result = turn.apps.call(app, number, "payment_app", "process_payment", {
amount = 100,
currency = "USD"
})

assert(result.success == true)
assert(result.transaction_id == "txn_123")

turn.test.assert_app_called(app_name, event_name, expected_data)

Verify that an app was called with specific data.

-- Call an app
turn.apps.call(app, number, "sms_app", "send_sms", {
phone = "27123456789",
message = "Hello"
})

-- Verify it was called correctly
turn.test.assert_app_called("sms_app", "send_sms", {
phone = "27123456789",
message = "Hello"
})

turn.test.get_app_calls()

Get all app calls made during the test.

-- Make some app calls
turn.apps.call(app, number, "sms_app", "send", {})
turn.apps.call(app, number, "email_app", "send", {})

-- Get all calls
local calls = turn.test.get_app_calls()
assert(#calls == 2)
assert(calls[1].app_name == "sms_app")
assert(calls[2].app_name == "email_app")

Contact and Journey Management

turn.test.add_contact(contact_data)

Add additional test contacts beyond the 2 pre-populated ones.

turn.test.add_contact({
uuid = "custom-contact",
name = "Custom Contact",
wa_id = "27111111111",
fields = {
age = 25,
city = "Cape Town"
}
})

local contact = turn.test.get_contact("custom-contact")
assert(contact.name == "Custom Contact")
assert(contact.fields.city == "Cape Town")

turn.test.get_contact(uuid)

Retrieve a contact by UUID.

local contact = turn.test.get_contact("contact-1")
assert(contact.name == "Test Contact 1")

turn.test.get_contacts()

Get all test contacts.

local contacts = turn.test.get_contacts()
assert(#contacts == 2) -- Pre-populated contacts

turn.test.add_journey(journey_data)

Add test journeys for testing journey-related functionality.

turn.test.add_journey({
uuid = "custom-journey",
name = "Payment Flow",
description = "Handles payments"
})

local journey = turn.test.get_journey("custom-journey")
assert(journey.name == "Payment Flow")

turn.test.get_journey(uuid)

Retrieve a journey by UUID.

local journey = turn.test.get_journey("journey-1")
assert(journey.name == "Test Journey 1")

Lease Testing

turn.test.add_lease(lease_data)

Add test leases for testing asynchronous operations that use the lease system.

turn.test.add_lease({
lease_id = "test-lease-1",
data = {
step = "waiting",
user_id = "123"
},
expires_at = os.time() + 300 -- 5 minutes from now
})

local lease = turn.test.get_lease("test-lease-1")
assert(lease.data.step == "waiting")

turn.test.get_lease(lease_id)

Retrieve a test lease by ID.

local lease = turn.test.get_lease("test-lease-1")
if lease then
assert(lease.data.user_id == "123")
end

Manifest Testing

turn.test.create_test_manifest(overrides)

Create a test manifest with sensible defaults, optionally overriding specific fields.

local manifest = turn.test.create_test_manifest({
app = {
name = "test_app",
version = "1.0.0",
title = "Test Application"
},
journeys = {
{name = "welcome", file = "journeys/welcome.md"},
{name = "onboarding", file = "journeys/onboard.md"}
},
contact_fields = {
{type = "text", name = "user_id", display = "User ID"}
}
})

-- Use in install tests
local result = turn.manifest.install(manifest)

turn.test.assert_journey_exists(manifest, journey_name)

Verify that a journey exists in the manifest.

local manifest = turn.test.create_test_manifest({
journeys = {
{name = "payment_flow", file = "journeys/payment.md"}
}
})

turn.test.assert_journey_exists(manifest, "payment_flow") -- Passes
-- turn.test.assert_journey_exists(manifest, "missing") -- Would fail

turn.test.get_journey_by_name(manifest, journey_name)

Retrieve a journey from the manifest by name.

local journey = turn.test.get_journey_by_name(manifest, "payment_flow")
assert(journey.file == "journeys/payment.md")

Logger Testing

turn.test.get_log_messages(level)

Retrieve log messages captured during the test, optionally filtered by log level. Useful for verifying that your app logs appropriate messages.

Parameters:

  • level (string, optional): Filter by log level ("debug", "info", "warning", "error"). If omitted, returns all log messages.

Returns an array of log entries, each containing:

  • level (string): The log level
  • message (string): The log message
  • timestamp (number): When the log was recorded
-- Get all log messages
local all_logs = turn.test.get_log_messages()

-- Get only error logs
local errors = turn.test.get_log_messages("error")
for _, log in ipairs(errors) do
print(log.level .. ": " .. log.message)
end

-- Get warning logs
local warnings = turn.test.get_log_messages("warning")
assert(#warnings == 0, "Expected no warnings")

turn.test.assert_logged(level, pattern)

Assert that a message matching a Lua pattern was logged at the specified level. Returns true if a matching log entry was found, false otherwise.

Parameters:

  • level (string): The log level to check ("debug", "info", "warning", "error")
  • pattern (string): A Lua pattern to match against log messages
-- Verify specific messages were logged
turn.logger.info("Processing order 12345")
turn.logger.error("Failed to connect to payment API")

-- Assert messages were logged
assert(turn.test.assert_logged("info", "Processing order"))
assert(turn.test.assert_logged("error", "payment API"))

-- Pattern matching works with Lua patterns
assert(turn.test.assert_logged("info", "order %d+")) -- Matches "order" followed by digits

Example: Testing Logging Behavior

describe("Order processing", function()
before(function()
turn.test.reset() -- Clears captured logs
end)

it("should log order processing steps", function()
-- Process an order (your app code)
App.on_event(app, number, "journey_event", {
function_name = "process_order",
args = {"ORD-123", 99.99}
})

-- Verify appropriate logging occurred
assert(turn.test.assert_logged("info", "Processing order ORD%-123"))
assert(turn.test.assert_logged("info", "Order total: 99.99"))

-- Verify no errors were logged
local errors = turn.test.get_log_messages("error")
assert(#errors == 0, "Expected no errors during processing")
end)

it("should log errors on invalid input", function()
App.on_event(app, number, "journey_event", {
function_name = "process_order",
args = {nil, -100}
})

-- Verify error was logged
assert(turn.test.assert_logged("error", "Invalid order"))
end)
end)

Spy Testing

turn.test.spy(func)

Create a spy that wraps a function and tracks calls for testing purposes. Returns a callable table that records all invocations with their arguments. This is Luerl-compatible and doesn't require C modules or the debug library.

Parameters:

  • func (function, optional): The function to wrap. If nil, creates a no-op spy.

Returns a callable spy object with:

  • .calls (array): List of call records, each with .vals (arguments array)
  • .call_count (number): Total number of times the spy was called
local turn = require("turn")

-- Create a spy wrapping a function
local mock_client = {
get = turn.test.spy(function(self, resource_type, id)
return {
data = {
resourceType = resource_type,
id = id,
name = "Test Resource"
}
}
end)
}

-- Call the spied function
local result = mock_client:get("Patient", "patient-123")

-- Verify calls
assert(mock_client.get.call_count == 1, "Expected get to be called once")
assert(#mock_client.get.calls == 1, "Expected 1 call record")
assert(mock_client.get.calls[1].vals[2] == "Patient", "Expected Patient resource type")
assert(mock_client.get.calls[1].vals[3] == "patient-123", "Expected patient ID")

-- Verify return value still works
assert(result.data.id == "patient-123", "Expected spy to return actual result")

Use spies to verify function calls in your tests:

describe("API client", function()
local client

before(function()
turn.test.reset()

client = {
create = turn.test.spy(function(self, resource)
return { id = "new-123" }
end),
update = turn.test.spy(function(self, id, resource)
return { id = id, updated = true }
end)
}
end)

it("should create and update resources", function()
-- Perform operations
local created = client:create({ name = "Test" })
local updated = client:update("new-123", { name = "Updated" })

-- Verify create was called
assert(client.create.call_count == 1, "Expected create to be called")
assert(client.create.calls[1].vals[2].name == "Test", "Expected correct data")

-- Verify update was called
assert(client.update.call_count == 1, "Expected update to be called")
assert(client.update.calls[1].vals[2] == "new-123", "Expected correct ID")
end)
end)