Testing and Debugging
Testing with the SDK
Inline execution
The fastest way to test Zymba code:
zeysdk run --zymba '$x = 42; echo $x * 2;'
The output includes execution metrics:
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:
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:
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:
$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:
@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:
$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:
$complex = [a: [1, 2], b: [x: "hello"]];
echo @Var.toSource($complex);
Stack traces
Exception objects include stack traces for debugging:
try {
$riskyOperation();
} catch ($e) {
echo "Error: " . $e.getMessage() . "\n";
echo "Trace: " . $e.getTracesAsString() . "\n";
}
Test patterns
Assertion helper
Build a simple assertion function:
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:
$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:
// 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: "alice@test.com", status: 1],
[ID: 2, name: "Bob", email: "bob@test.com", status: 0],
[ID: 3, name: "Charlie", email: "charlie@test.com", 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
// Bug: produces 0 (numeric addition of non-numeric strings)
$msg = "Error: " + $detail;
// Fix: use . for concatenation
$msg = "Error: " . $detail;
Bug: missing $this in methods
// 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
// 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.
// 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
// 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
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
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:
$tempFile = @IO.createTempFile("export");
try {
@IO.writeCSV($tempFile, $data, ",");
// ... process file
} finally {
@IO.deleteIfExists($tempFile);
}
See also
- Best Practices — production patterns and conventions
- Exceptions —
try/catch/finallyin depth