Skip to main content

Testing and Debugging

Testing with the SDK

Inline execution

The fastest way to test Zymba code:

Bash
zeysdk run --zymba '$x = 42; echo $x * 2;'

The output includes execution metrics:

Text
Metrics
Load Time | 0.8 ms
Execution Time | 0.1 ms
Memory Usage | 436.28 KB
Output Size | 2 Bytes
Output
84

Testing functions

Test individual functions in isolation:

Bash
zeysdk run --zymba '
function $calculateDiscount($price, $percent) {
if ($percent < 0 || $percent > 100) {
throw new @Exception("Invalid discount");
}
return $price * (1 - $percent / 100);
}

echo $calculateDiscount(100, 15);
echo " ";
echo $calculateDiscount(200, 25);
'

Testing error paths

Verify that error handling works correctly:

Bash
zeysdk run --zymba '
try {
$data = @Var.fromJSON("invalid json");
echo "Should not reach here";
} catch ($e) {
echo "Caught: " . $e.getMessage();
}
'

Debugging techniques

Echo debugging

The simplest approach — output values at key points:

ZYMBA
$data = @Var.fromJSON($input);
echo "Parsed data: " . @Var.toJSON($data) . "\n";

$filtered = @Array.filter($data.items, function($item) {
return $item.active;
});
echo "Filtered count: " . count $filtered . "\n";

@Console.log

Use @Console.log() for structured debug output that doesn't interfere with the HTTP response body:

ZYMBA
@Console.log("Processing order", $orderId);
@Console.log("Items:", @Var.toJSON($items));

// Retrieve collected messages
$messages = @Console.listMessages();
$summary = @Console.getSummary();

Type inspection

When a value behaves unexpectedly, inspect its type:

ZYMBA
$value = $getData();

echo "Type: " . typeof $value . "\n";
echo "Is object: " . ($value is object) . "\n";
echo "Is null: " . ($value is null) . "\n";
echo "Empty: " . (empty $value) . "\n";
echo "Exists: " . (exists $value) . "\n";

if ($value is object) {
echo "Keys: " . @Var.toJSON(@Array.listKeys($value)) . "\n";
}

@Var.toSource

Get a complete, human-readable representation of any value, including nested structures:

ZYMBA
$complex = [a: [1, 2], b: [x: "hello"]];
echo @Var.toSource($complex);

Stack traces

Exception objects include stack traces for debugging:

ZYMBA
try {
$riskyOperation();
} catch ($e) {
echo "Error: " . $e.getMessage() . "\n";
echo "Trace: " . $e.getTracesAsString() . "\n";
}

Test patterns

Assertion helper

Build a simple assertion function:

ZYMBA
function $assert($condition, $message = "Assertion failed") {
unless ($condition) {
throw new @Exception($message);
}
}

function $assertEqual($actual, $expected, $label = "") {
if ($actual !== $expected) {
throw new @Exception(
"assertEqual failed" . ($label ? " ($label)" : "")
. ": expected " . @Var.toJSON($expected)
. " but got " . @Var.toJSON($actual)
);
}
}

// Usage
$assertEqual(1 + 1, 2, "basic math");
$assertEqual(@String.trim(" hi "), "hi", "trim");
$assertEqual(@Array.count([1,2,3]), 3, "count");
echo "All tests passed";

Test runner pattern

Organize tests into named test cases:

ZYMBA
$tests = [
"string trim": function() {
$assertEqual(@String.trim(" hello "), "hello");
},
"array sort": function() {
$result = @Array.sortAscByValues([3, 1, 2]);
$assertEqual(@Array.firstValue($result), 1);
},
"json roundtrip": function() {
$data = [name: "test", value: 42];
$json = @Var.toJSON($data);
$parsed = @Var.fromJSON($json);
$assertEqual($parsed.name, "test");
$assertEqual($parsed.value, 42);
}
];

$passed = 0;
$failed = 0;

for ($tests as $name: $test) {
try {
$test();
$passed++;
echo "PASS: $name\n";
} catch ($e) {
$failed++;
echo "FAIL: $name - " . $e.getMessage() . "\n";
}
}

echo "\n$passed passed, $failed failed\n";

Testing with mock data

When testing code that normally queries a database, provide mock data:

ZYMBA
// Instead of:
// $users = $db.fetchAll(@SQL.prepare("SELECT * FROM contacts WHERE status = ?", 1), true);

// Use mock data for testing:
$users = [
[ID: 1, name: "Alice", email: "[email protected]", status: 1],
[ID: 2, name: "Bob", email: "[email protected]", status: 0],
[ID: 3, name: "Charlie", email: "[email protected]", status: 1]
];

$active = @Array.filter($users, function($u) { return $u.status == 1; });
$assertEqual(count $active, 2, "should have 2 active users");

Common bugs and fixes

Bug: using + for string concatenation

ZYMBA
// Bug: produces 0 (numeric addition of non-numeric strings)
$msg = "Error: " + $detail;

// Fix: use . for concatenation
$msg = "Error: " . $detail;

Bug: missing $this in methods

ZYMBA
// Bug: creates a local variable, doesn't update the object
$Counter = new object() {
value = 0;
increment() {
$value++; // Wrong!
}
};

// Fix: use $this
$Counter = new object() {
value = 0;
increment() {
$this.value++;
}
};

Bug: missing use in closures

ZYMBA
// Bug: $threshold is undefined inside the closure
$threshold = 100;
$isExpensive = function($p) { return $p > $threshold; };

// Fix: capture with use
$isExpensive = function($p) use ($threshold) { return $p > $threshold; };

Bug: expecting use to capture by reference

use copies the value at the time the closure is created. Mutations inside the closure don't affect the outer variable.

ZYMBA
// Bug: expects $total to accumulate
$total = 0;
@Array.forEach($items, function($item) use ($total) {
$total += $item.price; // Modifies local copy only
});
echo $total; // Still 0!

// Fix: use reduce instead
$total = @Array.reduce($items, 0, function($acc, $item) {
return $acc + $item.price;
});

Bug: treating "0" as truthy

ZYMBA
// Bug: "0" is falsy in Zymba!
$value = "0";
if ($value) {
// This block does NOT execute
}

// Fix: test explicitly for what you mean
if ($value !== null && $value !== "") {
// This works for any non-empty, non-null value
}

Error handling best practices

Validate input early

ZYMBA
function $processOrder($input) {
$order = null;
try {
$order = @Var.fromJSON($input);
} catch ($e) {
throw new @Exception("Invalid JSON input");
}

unless ($order is object) {
throw new @Exception("Order must be an object");
}
unless (exists $order.items) {
throw new @Exception("Order must have items");
}
if (empty $order.items) {
throw new @Exception("Order items cannot be empty");
}

// All preconditions met — safe to process
}

Wrap external calls

ZYMBA
function $fetchExternalData($url) {
try {
$response = @HTTP.requestBody($url, "GET");
return @Var.fromJSON($response);
} catch ($e) {
@Console.log("External API error: " . $e.getMessage());
throw new @Exception("Failed to fetch data from external service");
}
}

Clean up resources

Use finally to guarantee cleanup even when an exception is thrown:

ZYMBA
$tempFile = @IO.createTempFile("export");
try {
@IO.writeCSV($tempFile, $data, ",");
// ... process file
} finally {
@IO.deleteIfExists($tempFile);
}

See also