Mother DocsMother Docs
Buy me a Coffee
Steam Workshop
Discord
  • Cheatsheet
  • Mother OS (Ingame Script)
  • Mother GUI
  • Mother Autopilot System (MAPS)
  • Mother Core (Script Framework)
  • Motherland
  • Brand Guidelines
Buy me a Coffee
Steam Workshop
Discord
  • Cheatsheet
  • Mother OS (Ingame Script)
  • Mother GUI
  • Mother Autopilot System (MAPS)
  • Mother Core (Script Framework)
  • Motherland
  • Brand Guidelines
  • Cheatsheet
  • Mother OS (Ingame Script)
    • Getting Started

      • Upgrade Guide
      • Installation
      • Command Line Interface (CLI)
      • Configuration
      • Modules
    • Core Modules

      • Activity Monitor
      • Almanac
      • Block Catalogue
      • Intergrid Message Service
      • Local Storage
      • Merge Block Module
    • Extension Modules

      • Air Vent Module
      • Battery Module
      • Terminal Block Module
      • Cockpit Module
      • Connector Module
      • Display Module
      • Door Module
      • Gas Tank Module
      • Hinge Module
      • Landing Gear Module
      • Light Module
      • Piston Module
      • Programmable Block Module
      • Rotor Module
      • Screen Module
      • Sensor Module
      • Sorter Module
      • Sound Block Module
      • Thruster Module
      • Timer Block Module
      • Wheel Module
    • Compatibility
    • Examples
  • Mother GUI
    • Getting Started

      • Installation
      • Configuration
    • Commands
    • Menus
    • Views
  • Mother Autopilot System (MAPS)
    • Getting Started

      • Upgrade Guide
      • Installation
    • Modules

      • Flight Planning Module
      • Map Module
      • Flight Control Module
      • Attitude Module
      • Docking Module
  • Mother Core (Script Framework)
    • Getting Started

      • Upgrade Guide
      • Installation
      • Architecture Overview
      • Managing Script Size & Complexity
    • Building A Module
    • Mother CLI (Console)
    • Core Modules
      • Activity Monitor
      • Almanac
      • Block Catalogue
      • Clock
      • Command Bus
      • Configuration
      • Event Bus
      • Intergrid Message Service
      • Local Storage
      • Log
      • Terminal
    • Utilities

      • Color Helper
      • Number Helper
      • Security
      • Serializer
    • Testing
    • Tutorials
  • Motherland
  • Powered By Mother
  • Brand Guidelines

Mother Testing Framework

This framework is designed to emulate the Space Engineers game world and provide access to a representative environment for the programmable block's Program class. It simulates the game clock, IGC and other Program methods and properties. This makes it easy to test your scripts without ever booting Space Engineers. It also supports setup of a multi-script environment for testing across multiple Program instances. Developers can test against their own program (ie. Mother OS), or develop modules and commands using a mock Program instance.

  • Quick Example
  • Installation
  • Architectural Overview
  • Configuration
  • Running Tests
  • Testing a World
    • Setting up a basic world
    • Adding grids to a world
    • Adding scripts to a world
    • Communicating Between Scripts
    • Controlling the Clock
  • Testing a Script
    • Booting a script
    • Setup a script on a specific network
    • Using Mother
    • Accessing Modules
    • Running Terminal Commands
    • Using Events
    • Running the Program
    • Connecting Grids with Mechanical Blocks
    • Connecting Grids with Merge Blocks
  • Testing a Module
  • Available Assertions
    • World Assertions
    • Script Assertions
    • Module Assertions
  • Generic Program Support
  • Examples

Why does this framework exist?

I grew tired of booting and testing Mother OS in-game. So much of my work did not require a live game instance, so as I have built several scripts with Mother Core, I have cohered a toolset that makes building on top of this core extremely simple. As MAPS takes flight, this framework will be instrumental in keeping our Engineers alive. I aim to enable script developers with a new level of confidence and agility as they build the next generation of Space Engineers programmable block scripts.

If you ask 'Should we be in space?' you ask a nonsense question. We are in space. We will be in space ― Frank Herbert

Quick Example

We want to test the light/color command. We boot a Script, attach a light block to its grid, and then run the command via the terminal. We validate that the light has changed color and the command has been executed.

LightModule.Tests.cs
[Test]
public void LightColor_Command_Can_Set_Searchlight_Color()
{
    var searchlight = TerminalBlockFactory.Create<IMySearchlight>("Beacon");

    var script = ScriptFactory<Program>()
        .WithMother()
        .WithBlock(searchlight)
        .Boot();

    script.RunTerminal("light/color Beacon red");

    Assert.That(searchlight.Color, Is.EqualTo(new Color(255, 0, 0)));
    script.ShouldHaveExecuted("light/color");
}

Not bad. The fluency of this interface is maintained throughout this framework and aims to make writing tests as frictionless as possible.

Installation

This testing framework comes with every new installation of Mother Core using Mother CLI. Otherwise, it can be copied manually from the Mother Core project on GitHub and been loaded as a *.Tests.proj project.

Caution

You must make the Program class public if you are adding this testing suite to an existing project. This ensure the test suite can integrate with your Program correctly.

Program.cs
namespace IngameScript
{
    public partial class Program : MyGridProgram
}

Architectural Overview

The test harness is built around a few layers:

  • World: Multi-script topology, communication, and orchestration.
  • Script: Wrapper for a single booted programmable block Program.cs instance.
  • Module: A single module containing custom behavior in a booted script.
  • Command: Player command execution behavior (usually tested from module or script context).

Tips

When in doubt, write a Script-based test. This is easy to change later and immediately situates you in the context of a single script that can send and receive communications on a simulated clock system.

Configuration

The framework keeps the same compatibility contract as your programmable blocks script.

  • Target framework: netframework48
  • Language version: C# 6
Script.proj
<TargetFramework>netframework48</TargetFramework>
<LangVersion>6</LangVersion>

Running Tests

Run the full MotherCore test project:

PowerShell
dotnet test .\MotherOS\tests\MotherOS.Tests\MotherOS.Tests.csproj

Run focused slices:

PowerShell
dotnet test .\MotherOS\tests\MotherOS.Tests\MotherOS.Tests.csproj --filter "FullyQualifiedName~LightModuleTests"

Testing a World

Use world-first setup when testing multiple scripts, command routing, or intergrid communication.

Setting up a basic world

When booting a world, we are also creating all of the necessary framework to store knowledge of grids, and broker messages between then with a mock IGC.

*.Tests.cs
// Boot a generic world
var world = WorldFactory().Boot();

Adding grids to a world

Use world/grid helpers before booting scripts when you need explicit topology or block placement on specific grids.

*.Tests.cs
// Boot world
var world = WorldFactory().Boot();

// create grid within world
var grid = world.CreateGrid();

// create grid with a custom name
var grid = world.CreateGrid("Mothership");

Adding scripts to a world

*.Tests.cs
// Boot world
var world = WorldFactory().Boot();

// Boot a script with Mother Core loaded
var sender = world.CreateScript("Sender").WithMother().Boot();

Communicating Between Scripts

Scripts can easily join the same IGC network with the OnNetwork method of the Script object. It accepts an optional argument where a specific network can be provided. Using OnNetwork without an argument will join the default world IGC.

*.Tests.cs
// Boot world
var world = WorldFactory().Boot();

// Create two scripts on the same default network, with Mother loaded
var scriptA = world.CreateScript("scriptA")
    .WithMother()
    .OnNetwork()
    .Boot();

var scriptB = world.CreateScript("scriptB")
    .WithMother()
    .OnNetwork()
    .Boot();

// Run a terminal action on scriptA
scriptA.RunTerminal("@scriptB help");

// Deliver messages through network
world.DeliverMessages();

// Assert scriptA send a message to scriptB
world.ShouldHaveDeliveredIgcMessage(scriptA, scriptB, "*");
world.ShouldHaveNoPendingMessages();

// Assert communication was delivered
scriptB.ShouldHaveExecuted("help");

We can also define a custom network for scenarios where we want to manage multiple within a single gameworld to emulate factions, etc.

*.Tests.cs
// Create a custom network
var customNetwork = new FakeIGCNetwork();

// Create a script on the custom network
var script = world.CreateScript()
    .WithMother()
    .OnNetwork(customNetwork)
    .Boot();

We use the DeliverMessages() method on the World object to simulate the delivery of all messages queued in the IGC via the SendBroadcastMessage() and SendUnicastMessage() methods of IMyIntergridCommunicationSystem.

  • DispatchIgc() when you need transport-only dispatch.

Controlling the Clock

  • Tick(...) for full world cycles.

Testing a Script

If you do not need to worry about world-level configuration, or a multi-script setup, then you can use the ScriptFactory to quickly setup scripts for testing. We can create a generic Program, or one built with Mother Core. To test a specific program instance, we use the Program as a type argument.

Booting a script

*.Tests.cs
// Boot a generic script (MDK2 default)
var script = ScriptFactory().Boot();

// Boot a script with Mother Core
var script = ScriptFactory().WithMother().Boot();

// Boot a specific instance of a generic Program 
var script = ScriptFactory<Program>().Boot();

// Boot a specific instance of a Mother Program
// ie. MotherOS.Program, MotherGUI.Program
var script = ScriptFactory<Program>().WithMother().Boot();

Setup a script on a specific network

*.Tests.cs
// Create script with new "MainNetwork" IGC network 
// that can be joined by other scripts
var script = ScriptFactory()
    .WithMother()
    .OnNetwork("MainNetwork")
    .Boot();

// Create script with a custom network
var customNetwork = new FakeIGCNetwork();

// Create a script on the custom network
var script = world.CreateScript()
    .WithMother()
    .OnNetwork(customNetwork)
    .Boot();

Using Mother

When our script has Mother Core installed, we can take advantage of the Mother property on the script as an easy accessor to our Mother instance. This allows you to circumvent any interaction with the Program class.

*.Tests.cs
// Create a script
var script = ScriptFactory().WithMother().Boot();

// Access Mother's awesomeness
var mother = script.Mother;

Accessing Modules

The Boot method generates a script with a fully-booted instance of the Program. If no Program argument is provided, Mother boots with no Extension Modules.

*.Tests.cs
// Create a script with Mother OS Program instance
var script = ScriptFactory<MotherOS.Program>().WithMother().Boot();

// Get module from Mother
var module = script.Mother.GetModule<LightModule>();

Running Terminal Commands

We can easily simulate a terminal command using the RunTerminal method. This simulates a terminal input which Mother Core uses to trigger activity.

*.Tests.cs
// Boot a script with Mother
var script = ScriptFactory<Program>().WithMother().Boot();

// Run a terminal command
script.RunTerminal("rename Frigate");

// Assert command was executed
script.ShouldHaveExecuted("rename");
Assert.That(script.Mother.name, Is.EqualTo("Frigate"));

Using Events

We can test that an event has been fired by a module using the AssertEventEmitted() method on the Script object:

*.Tests.cs
// Create a door block
var door = TerminalBlockFactory.Create<IMyDoor>(customName: "Airlock");

// Boot a script with the door
var script = ScriptFactory<Program>()
    .WithMother()
    .WithBlock(door)
    .Boot();

// Clear any existing events
script.ClearEventEmissions();

// Helper to manually set the door status
SetDoorStatus(door, DoorStatus.Opening);

// Run any queue actions in Mother's clock
script.RunToIdle();

// Verify that event was emitted when the door status changed
script.AssertEventEmitted<DoorOpeningEvent>();

Running the Program

The RunToIdle method runs down any queued activity in the ClockModule to ensure all actions complete.

*.Tests.cs
// Run with script-default update type
script.Run()

// Run with custom update type and empty argument
script.Run(UpdateType.Update10)

// Helper for running UpdateType.Terminal and UpdateType.Trigger
script.RunTerminal("ping")
script.RunTrigger("help")

// Execute any activity queued in Clock
script.RunToIdle();

Connecting Grids with Mechanical Blocks

One of Mother Core's highest value proposition is the resolution of grid vs. construct when it comes to connectors and mechanical block connections. We can use the ConnectGrids method to simulate the joining of two grids together via a Rotor, Hinge, or Piston.

*.Tests.cs
// Create our script with an associated programmable block and grid
var script = ScriptFactory().WithMother().Boot();

// create second grid
var cargoGrid = GridFactory.Create("Cargo Pod");

// Connect to grids with a mechanical block - default = Rotor
var mechanicalBlock = script.ConnectGrids(script.PrimaryGrid, cargoGrid);

// Or connect with a specific connection type
var mechanicalBlock = script.ConnectGrids(
    script.PrimaryGrid, 
    cargoGrid,
    MechanicalConnectionKind.Piston
);

We can then assert that grids are on the same construct now:

*.Tests.cs
script.ShouldBeSameConstruct(script.PrimaryGrid, cargoGrid);

Connecting Grids with Merge Blocks

Merge blocks are tested similarly, but the merge state is controlled through MergeBlockModule. A simple pattern is: create two grids, connect with a merge block, lock it, then assert both grids now resolve as the same construct.

*.Tests.cs
// Boot world and add grids
var world = WorldFactory().Boot();
var carrierGrid = world.CreateGrid("Carrier");
var cargoGrid = world.CreateGrid("Cargo Pod");

// Create and associate a merge block to each grid
var mergeBlockA = TerminalBlockFactory.Create<IMyShipMergeBlock>(
    customName: "MergeA",
    customData: new CustomDataComposer()
        .With("hooks", "onMerge", "rename CarrierMerged")
        .Build()
);
var mergeBlockB = TerminalBlockFactory.Create<IMyShipMergeBlock>(customName: "MergeB");

// Boot script on existing grid within world
var script = world.CreateScript(carrierGrid).WithMother().Boot();

// simulate a merge between two blocks
world.MergeBlocks(mergeBlockA, mergeBlockB);

// Run the script
script.RunToIdle();

// Assert events were fired
script.AssertEventEmitted<MergeBlockLockedEvent>();
script.AssertEventEmitted<ConstructRefreshedEvent>();

// assert hook was called
script.AssertCommandExecuted("rename");
Assert.That(script.Mother.Name, Is.EqualTo("CarrierMerged"));

// assert construct/grid configuration
var catalogue = script.Mother.GetModule<BlockCatalogue>();
Assert.That(catalogue.ConstructGridIds, Has.Count.EqualTo(2));
Assert.That(catalogue.GetBlocksByName<IMyShipMergeBlock>("MergeA"), Has.Count.EqualTo(1));
Assert.That(catalogue.GetBlocksByName<IMyShipMergeBlock>("MergeB"), Has.Count.EqualTo(1));

Testing a Module

When we are focused on the logic within a single module, we can use the ModuleFactory to create the module instance. We provide module and program type arguments to configure our script.

LightModule.Tests.cs
// Create a LightModule instance in the Program instance
var module = ModuleFactory<LightModule, Program>().Boot();

// Call a method on the module
module.SetColor(...)

Available Assertions

Use helper assertions first, then inspect low-level transport or counters only when necessary.

World Assertions

  • world.ShouldHaveDeliveredIgcMessage(sender, receiver, "*")
  • world.ShouldHaveNoPendingMessages()

Script Assertions

  • script.ShouldHaveExecuted("command")
  • script.ShouldHavePrinted("text")
  • script.ShouldBeSameConstruct(gridA, gridB)
  • script.ClearEventEmissions()
  • script.AssertEventEmitted<TEvent>()
  • script.AssertEventEmitted<TEvent>(count)

Module Assertions

Module tests generally assert module behavior and state with your test framework (for example, NUnit):

  • Assert.That(actual, Is.EqualTo(expected))
  • Assert.That(condition, Is.True)

When module behavior emits script-level events or terminal activity, use script assertions from the module test context.

Generic Program Support

The harness works with scripts scaffolded by MDK2 and booted from MyGridProgram. You can still take advantage of all World and Script helpers that do not relate to Mother.

*.Tests.cs
var script = ScriptFactory<Program>().Boot();
var program = script.Program;

program.Echo("Hello")
script.ShouldHavePrinted("Hello");

Important

Developers building on Mother Core get richer world/script/module/command helpers while staying compatible with the baseline MyGridProgram model.

Examples

Modules

LightModule.Tests.cs
[Test]
public void SetColor_Sets_Color_For_Lighting_Blocks()
{
    var module = ModuleFactory<LightModule, Program>().Boot();
    var light = TerminalBlockFactory.Create<IMyLightingBlock>();
    var searchlight = TerminalBlockFactory.Create<IMySearchlightBlock>();

    module.SetColor(light, Color.Blue);
    module.SetColor(searchlight, Color.Red);

    Assert.That(light.Color, Is.EqualTo(Color.Blue));
    Assert.That(searchlight.Color, Is.EqualTo(Color.Red));
}
Last Updated: 9/29/26, 10:19 PM
Contributors: lukejamesmorrison
Next
Tutorials