Testing Plugins
The Foresight SDK includes a specialized testing harness called FseAppTest. This harness is designed to help you write robust unit and integration tests for your plugins, allowing you to verify your Statements and Tools in a headless environment—without ever needing to launch the full 3D client.
Simulation with FseAppTest
FseAppTest provides a controlled Bevy engine where you can simulate project state and user actions. By using this harness, you can precisely control the timing of events and assert that your plugin’s logic behaves exactly as expected.
To use the harness, you define an App with your plugin’s dependencies and then use AppStep to describe a sequence of actions. This measurement test shows how to verify that a mutation correctly spawns a new object:
use fsl_testing::{AppStep, FseAppTest};
use bevy::prelude::*;
#[test]
fn test_my_mutation() {
let mut app = App::new();
app.add_plugins(MyPlugin);
FseAppTest::<()>::fse_state_app(app)
.with_step(AppStep::single_update(|world, _| {
// Manually trigger a state change
let mut fse = world.resource_mut::<Fse>();
fse.queue_auto("spawn object", |fse| {
async move {
fse.new_object::<MyObject>(())?;
Ok(StepResult::Continue)
}.boxed()
});
}))
.with_step(AppStep::run_system(|fse: Res<Fse>| {
// Verify the object was created in the project state
assert_eq!(fse.get_objects_of_type::<MyObject>().len(), 1);
}))
.run();
}Headless Tool Testing
Testing an asynchronous Tool follows a similar pattern but adds the complexity of time. Since tools wait for user input, your test must simulate those inputs across multiple engine frames.
Typically, you will start the tool’s run method in a background task and then use AppStep::single_update to send property updates or simulated clicks to the tool’s input channel. By using AppStep::loop_until, you can pause the test until the tool has committed its final transaction, at which point you can inspect the global Fse resource to verify the results.
The Benefits of Statement Testing
Because Statements are serializable and deterministic, they are the highest-value target for your testing suite. We recommend writing three types of statement tests:
- Logic Verification: Ensuring that a mutation’s
applymethod correctly transforms the objects in the project hierarchy. - Edge Case Handling: Verifying that your statements handle invalid IDs, network timeouts, or corrupted data gracefully without crashing the client.
- Round-trip Serialization: Testing that a statement can be turned into JSON and back into a Rust struct without any data loss. This is critical for ensuring that user macros and session journals remain stable across plugin updates.
Best Practices for Reliable Tests
To keep your testing suite fast and maintainable, keep these design patterns in mind:
- Isolate Your State: Always start each test with a fresh
FseAppTestinstance. This prevents “state leakage” where a successful test in one part of your suite accidentally hides a failure in another. - Mock External Dependencies: If your plugin interacts with the Dagger database or local file system, use mocks to simulate those responses. This keeps your tests fast and removes the need for a live network connection during your build process.
- Sequence with AppStep: Use the step-based approach to reason about your test as a timeline of events. This makes it much easier to debug complex asynchronous workflows.