Single Source of Truth for All Testing Patterns
Last Updated: 2025-10-10
Status: Active - v0.0.4 Phase 5
Purpose: Comprehensive guide to all testing approaches in FerrisScript
FerrisScript uses a layered testing strategy where each layer validates different concerns:
┌─────────────────────────────────────────────┐
│ Layer 4: Manual Testing (Godot Editor) │ ← Feature validation
├─────────────────────────────────────────────┤
│ Layer 3: Integration Tests (.ferris) │ ← End-to-end behavior
├─────────────────────────────────────────────┤
│ Layer 2: GDExtension Tests (GDScript) │ ← Godot bindings
├─────────────────────────────────────────────┤
│ Layer 1: Unit Tests (Rust) │ ← Pure logic
└─────────────────────────────────────────────┘
test_harness and ferris-test.toml.ferris files include TEST metadata (see below)All .ferris example and test files now include standardized headers for test runner integration:
// TEST: test_name_here
// CATEGORY: unit|integration|error_demo
// DESCRIPTION: Brief description of what this tests
// EXPECT: success|error
// ASSERT: Expected output line 1
// ASSERT: Expected output line 2
// EXPECT_ERROR: E200 (optional, for negative tests)
//
// Additional documentation follows...
Example:
// TEST: hello_world
// CATEGORY: integration
// DESCRIPTION: Basic "Hello World" example demonstrating _ready() lifecycle hook
// EXPECT: success
// ASSERT: Hello from FerrisScript!
//
// This is the simplest FerrisScript example...
Benefits:
See: docs/testing/TEST_HARNESS_TESTING_STRATEGY.md for metadata parser details
FerrisScript/
├── crates/
│ ├── compiler/ # Layer 1: Unit tests (543 tests)
│ │ └── src/
│ │ ├── lexer.rs (tests inline)
│ │ ├── parser.rs (tests inline)
│ │ ├── type_checker.rs (tests inline)
│ │ └── error_code.rs (tests inline)
│ │
│ ├── runtime/ # Layer 1: Unit tests (110 tests)
│ │ ├── src/lib.rs (tests inline)
│ │ └── tests/
│ │ └── inspector_sync_test.rs
│ │
│ ├── godot_bind/ # Layer 1 + Layer 2
│ │ ├── src/lib.rs (11 unit tests pass, 10 ignored*)
│ │ └── tests/
│ │ └── headless_integration.rs (Layer 2: GDExtension tests)
│ │
│ └── test_harness/ # Layer 2 Infrastructure
│ ├── src/
│ │ ├── lib.rs
│ │ ├── main.rs (ferris-test CLI)
│ │ ├── godot_cli.rs (GodotRunner)
│ │ ├── output_parser.rs (Marker parsing)
│ │ └── test_runner.rs (TestHarness)
│ └── tests/
│ └── (self-tests)
│
├── godot_test/ # Layer 2 + Layer 3
│ ├── ferrisscript.gdextension
│ ├── scripts/
│ │ ├── *.ferris (Layer 3: Integration tests)
│ │ └── *.gd (Layer 2: GDExtension test runners)
│ └── tests/
│ └── generated/ (Auto-generated .tscn files)
│
├── ferris-test.toml # Shared Configuration
└── docs/
├── TESTING_GUIDE.md ← You are here
└── planning/v0.0.4/
├── TESTING_STRATEGY_PHASE5.md (Detailed strategy)
├── INTEGRATION_TESTS_REPORT.md (Phase 5 results)
└── INTEGRATION_TESTS_FIXES.md (Bug fixes)
Note: The 10 ignored godot_bind tests require Godot runtime. They’re covered by Layer 2 (GDExtension tests) and Layer 3 (integration tests). See Why Some Tests Are Ignored.
When to use: Pure logic without Godot dependencies
Location: Inline #[cfg(test)] mod tests in source files
Example: Compiler type checking, runtime value operations
// In crates/compiler/src/type_checker.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_export_range_hint_valid_i32() {
let source = r#"
@export @range(0, 100, 1)
global health: i32 = 50;
"#;
let result = type_check(source);
assert!(result.is_ok());
}
}
# All unit tests
cargo test
# Specific crate
cargo test --package ferrisscript_compiler
# Specific test
cargo test test_export_range_hint_valid_i32
# With output
cargo test -- --nocapture
Total: ~702 unit tests
When to use: End-to-end validation of FerrisScript → Godot compilation and execution
Location: godot_test/scripts/*.ferris
Infrastructure: test_harness crate + ferris-test.toml
.ferris script with test metadata commentsferris-test CLI (uses test_harness crate).tscn scene file[PASS]/[FAIL] markers// godot_test/scripts/export_properties_test.ferris
// @test-category: integration
// @test-name: Exported Properties with All Types and Hints
// @expect-pass
// Test exported properties
@export
global basic_int: i32 = 42;
@export @range(0, 100, 1)
global health: i32 = 100;
@export @enum("Small", "Medium", "Large")
global size: String = "Medium";
fn _ready() {
print("[TEST_START]");
// Test basic int
if basic_int == 42 {
print("[PASS] basic_int has correct value");
} else {
print("[FAIL] basic_int incorrect");
}
// Test range
if health >= 0 && health <= 100 {
print("[PASS] health within range");
} else {
print("[FAIL] health out of range");
}
print("[TEST_END]");
}
// @test-category: integration | unit | feature | regression
// @test-name: Human-readable test description
// @expect-pass | @expect-error(E301) | @expect-error-demo
// @assert: condition description (optional, multiple allowed)
# Run single test
ferris-test --script godot_test/scripts/export_properties_test.ferris
# Run all tests
ferris-test --all
# Filter by name
ferris-test --all --filter "export"
# Verbose output
ferris-test --all --verbose
# JSON format (for CI)
ferris-test --all --format json > results.json
# Location: workspace root
godot_executable = "Y:\\cpark\\Projects\\Godot\\Godot_v4.5-dev4_win64.exe\\Godot_v4.5-dev4_win64_console.exe"
project_path = "./godot_test"
timeout_seconds = 30
output_format = "console"
verbose = true
Environment Overrides:
GODOT_BIN: Override godot_executableGODOT_PROJECT_PATH: Override project_pathGODOT_TIMEOUT: Override timeout_secondsCurrent integration tests:
export_properties_test.ferris - All 8 types, 4 hint typesclamp_on_set_test.ferris - Range clamping behaviorsignal_test.ferris - Signal emissionprocess_test.ferris - Lifecycle functionsnode_query_*.ferris - Scene tree queriesstruct_literals_*.ferris - Godot type constructionbounce_test.ferris, move_test.ferris, hello.ferris - ExamplesTotal: 15+ integration tests
See: docs/planning/v0.0.4/INTEGRATION_TESTS_REPORT.md for detailed results
When to use: Testing Rust functions that construct Godot types (GString, PropertyInfo, etc.)
Location: crates/{crate}/tests/headless_integration.rs + godot_test/scripts/*.gd
Why needed: Some Rust functions require godot::init() which can’t run in unit tests
// In crates/godot_bind/src/lib.rs
fn map_hint(hint: &ast::PropertyHint) -> PropertyHintInfo {
match hint {
ast::PropertyHint::Range { min, max, step } => {
export_info_functions::export_range( // ← Requires godot::init()
*min as f64,
*max as f64,
Some(*step as f64),
// ...
)
}
// ...
}
}
#[test]
#[ignore = "Requires Godot engine runtime"]
fn test_map_hint_range() {
let hint = ast::PropertyHint::Range { min: 0.0, max: 100.0, step: 1.0 };
let result = map_hint(&hint); // ← FAILS: godot::init() not called
assert_eq!(result.hint, PropertyHint::RANGE);
}
Step 1: Create GDScript test runner
# godot_test/scripts/godot_bind_tests.gd
extends Node
var passed_tests: int = 0
var failed_tests: int = 0
func _ready():
print("[TEST_START]")
test_basic_functionality()
test_property_hint_enum()
# ... more tests
print("[SUMMARY] Total: %d, Passed: %d, Failed: %d" %
[passed_tests + failed_tests, passed_tests, failed_tests])
print("[TEST_END]")
get_tree().quit(failed_tests if failed_tests > 0 else 0)
func test_basic_functionality():
run_test("Basic Node Creation", func():
var node = Node.new()
assert_not_null(node, "Node should be created")
node.queue_free()
)
func test_property_hint_enum():
run_test("PropertyHint Enum Exists", func():
# Validate that PropertyHint enum is accessible
assert_equal(PropertyHint.NONE, 0, "PropertyHint.NONE should be 0")
assert_equal(PropertyHint.RANGE, 1, "PropertyHint.RANGE should be 1")
)
func run_test(test_name: String, test_func: Callable):
print("[TEST] Running: %s" % test_name)
var error = test_func.call()
if error == null:
print("[PASS] %s" % test_name)
passed_tests += 1
else:
print("[FAIL] %s - %s" % [test_name, error])
failed_tests += 1
func assert_equal(actual, expected, message: String):
if actual != expected:
return "%s (expected: %s, got: %s)" % [message, expected, actual]
return null
func assert_not_null(value, message: String):
if value == null:
return message
return null
Step 2: Create test scene
# godot_test/test_godot_bind.tscn
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/godot_bind_tests.gd" id="1"]
[node name="GodotBindTests" type="Node"]
script = ExtResource("1")
Step 3: Create Rust integration test
// crates/godot_bind/tests/headless_integration.rs
use ferrisscript_test_harness::{TestConfig, TestOutput, GodotRunner};
use std::path::PathBuf;
fn get_test_config() -> Result<TestConfig, String> {
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent().unwrap()
.parent().unwrap()
.to_path_buf();
let config_path = workspace_root.join("ferris-test.toml");
let mut config = if config_path.exists() {
TestConfig::from_file(&config_path)
.map_err(|e| format!("Failed to load config: {}", e))?
} else {
TestConfig::default()
};
config = config.with_env_overrides();
Ok(config)
}
#[test]
#[ignore = "Requires Godot executable - configure in ferris-test.toml"]
fn test_godot_headless_basic() {
let config = get_test_config().expect("Failed to load config");
let runner = GodotRunner::new(
config.godot_executable,
config.project_path,
config.timeout_seconds,
);
let test_scene = PathBuf::from("test_godot_bind.tscn");
let output = runner.run_headless(&test_scene)
.expect("Failed to run Godot");
// Parse [PASS]/[FAIL] markers
let passed = output.stdout.contains("[PASS]")
&& !output.stdout.contains("[FAIL]");
assert!(passed, "Test failed. Output:\n{}", output.stdout);
assert_eq!(output.exit_code, 0);
}
# Run ignored tests (requires Godot configured in ferris-test.toml)
cargo test --package ferrisscript_godot_bind --test headless_integration -- --ignored --nocapture
# Or use environment override
GODOT_BIN=/path/to/godot cargo test --package ferrisscript_godot_bind --test headless_integration -- --ignored
Add GDExtension tests when you have Rust functions that:
GString, PropertyInfo, Variant, etc.)godot::init() to runDon’t add GDExtension tests for:
.ferris script behavior (use integration tests)Current GDExtension tests:
Why 10 godot_bind tests are ignored: They’re low-level binding tests that require Godot runtime. The functionality IS tested via integration tests (export_properties_test.ferris validates all hint types work correctly). See Why Some Tests Are Ignored.
When to use: Performance regression detection
Location: crates/compiler/benches/*.rs
Infrastructure: Criterion.rs
// crates/compiler/benches/parser_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ferrisscript_compiler::parse;
fn bench_parse_hello(c: &mut Criterion) {
let source = r#"
fn _ready() {
print("Hello, world!");
}
"#;
c.bench_function("parse hello", |b| {
b.iter(|| parse(black_box(source)))
});
}
criterion_group!(benches, bench_parse_hello);
criterion_main!(benches);
# All benchmarks
cargo bench
# Specific benchmark
cargo bench --bench parser_bench
# With baseline comparison
cargo bench -- --baseline main
Current benchmarks:
See: docs/BENCHMARK_BASELINE.md for baseline results
# Godot executable path (console version recommended for CI)
godot_executable = "Y:\\cpark\\Projects\\Godot\\Godot_v4.5-dev4_win64.exe\\Godot_v4.5-dev4_win64_console.exe"
# Godot project directory
project_path = "./godot_test"
# Test timeout in seconds
timeout_seconds = 30
# Output format: "console", "json", or "tap"
output_format = "console"
# Enable verbose output
verbose = true
Override config values with environment variables:
# Windows (PowerShell)
$env:GODOT_BIN = "C:\Path\To\Godot.exe"
$env:GODOT_PROJECT_PATH = "C:\Path\To\godot_test"
$env:GODOT_TIMEOUT = "60"
# Linux/Mac
export GODOT_BIN="/path/to/godot"
export GODOT_PROJECT_PATH="/path/to/godot_test"
export GODOT_TIMEOUT="60"
{
"version": "2.0.0",
"tasks": [
{
"label": "Test: Unit Tests",
"type": "cargo",
"command": "test",
"group": {
"kind": "test",
"isDefault": true
}
},
{
"label": "Test: Integration Tests",
"type": "shell",
"command": "ferris-test",
"args": ["--all"],
"group": {
"kind": "test",
"isDefault": false
}
},
{
"label": "Test: GDExtension Tests",
"type": "shell",
"command": "cargo",
"args": [
"test",
"--package", "ferrisscript_godot_bind",
"--test", "headless_integration",
"--", "--ignored", "--nocapture"
],
"group": {
"kind": "test",
"isDefault": false
}
}
]
}
# Layer 1: Unit tests (fast, <1s)
cargo test
# Layer 2: GDExtension tests (requires Godot, ~5-10s)
cargo test --test headless_integration -- --ignored --nocapture
# Layer 3: Integration tests (requires Godot, ~30s)
ferris-test --all
# Layer 4: Manual testing
# Open godot_test/project.godot in Godot Editor
Pre-commit: Run fast unit tests
cargo test
Pre-push: Run unit + integration tests
cargo test && ferris-test --all
Feature validation: Run all layers
cargo test && \
cargo test --test headless_integration -- --ignored --nocapture && \
ferris-test --all
CI/CD: All automated tests
# In GitHub Actions workflow
cargo test --all
ferris-test --all --format json > integration-results.json
cargo bench -- --baseline main
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Run unit tests
run: cargo test --all
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Godot
run: |
wget https://downloads.tuxfamily.org/godotengine/4.3/Godot_v4.3-stable_linux.x86_64.zip
unzip Godot_v4.3-stable_linux.x86_64.zip
sudo mv Godot_v4.3-stable_linux.x86_64 /usr/local/bin/godot
chmod +x /usr/local/bin/godot
- name: Build GDExtension
run: cargo build --release
- name: Run integration tests
env:
GODOT_BIN: godot
run: |
cargo install --path crates/test_harness
ferris-test --all --format json > results.json
- name: Upload results
uses: actions/upload-artifact@v3
with:
name: integration-results
path: results.json
Location: crates/godot_bind/src/lib.rs
Tests:
test_map_hint_nonetest_map_hint_rangetest_map_hint_enumtest_map_hint_file_with_dotstest_map_hint_file_with_wildcardstest_map_hint_file_without_dotstest_metadata_basic_propertytest_metadata_with_range_hinttest_metadata_with_enum_hinttest_metadata_with_file_hintWhy Ignored: These tests call functions that construct Godot types (GString, PropertyHintInfo), which require godot::init(). This can’t be called in Rust unit tests because:
godot::init() would conflictAre They Tested?: YES! The functionality IS validated via:
export_properties_test.ferris tests all 8 types and 4 hint types end-to-endheadless_integration.rs can test these functions directly once FerrisScriptTestNode is addedShould They Be Enabled?: NO. They serve as documentation of the API but are redundant with higher-level tests. The ignore attribute correctly indicates these are low-level functions requiring Godot runtime.
Alternative Approach: If unit testing these functions is critical, they could be refactored to:
See: docs/planning/v0.0.4/TESTING_STRATEGY_PHASE5.md Section “godot_bind Tests (21 tests: 11 passing, 10 failing)”
Problem: Tests can’t find Godot
Solution:
ferris-test.toml has correct godot_executable pathGODOT_BIN environment variable# Windows
$env:GODOT_BIN = "C:\Godot\Godot_v4.3-stable_win64.exe"
# Linux
export GODOT_BIN="/usr/local/bin/godot"
Problem: Test exceeds timeout_seconds
Solution:
timeout_seconds in ferris-test.tomlGODOT_TIMEOUT environment variable--verbose flag)Problem: Godot can’t find test scene
Solution:
project_path points to godot_test/test_godot_bind.tscn, test_{name}.tscn)Problem: Godot can’t load FerrisScript GDExtension
Solution:
cargo build --releasegodot_test/ferrisscript.gdextension paths are correct.dll/.so exists in target/release/Problem: Parser misinterpreted output
Solution:
[TEST_START], [PASS], [FAIL], [TEST_END] markers[FAIL] markers in error messages--verbose to see full outputProblem: Environment differences
Solution:
docs/planning/v0.0.4/TESTING_STRATEGY_PHASE5.md - Detailed strategy and analysis (1533 lines)docs/planning/v0.0.4/INTEGRATION_TESTS_REPORT.md - Phase 5 test results and findingsdocs/planning/v0.0.4/INTEGRATION_TESTS_FIXES.md - Bug fixes from integration testingdocs/HEADLESS_GODOT_SETUP.md - GDExtension testing architecture (archival)docs/RUNNING_HEADLESS_TESTS.md - User guide (archival - superceded by this guide)docs/BENCHMARK_BASELINE.md - Performance baselinesdocs/DEVELOPMENT.md - General development guidedocs/archive/testing/TEST_HARNESS_TESTING_STRATEGY.md - Phase 3 test harness designdocs/archive/testing/PHASE_3_COMPLETION_REPORT.md - Phase 3 testing resultsSetting up testing for a new feature:
#[cfg(test)] mod tests for pure logic.ferris script in godot_test/scripts/ if testing end-to-end behaviorcrates/*/benches/ if performance-critical.github/workflows/)Questions or Issues? See CONTRIBUTING.md or open a GitHub issue.