It can be incredibly frustrating to write a script for Space Engineers, test it out locally in-game, with a very time consuming and limited grid, and have it fail for consumers. The time and effort required to test out some scripts in-game can be monumental, and even then the code can be brittle, vulnerable to changes by future mods and updates. I’ve experienced this issue with content of my own on the Steam Workshop.

One of the initiatives I’m starting with my own scripts is to fully unit test them in NUnit, and this guide will cover how I’m doing this. It’s a follow up to my previous guide to setting up MonoDevelop for compiling scripts for Space Engineers, so I’ll be using MonoDevelop, but these general steps should work in any standalone setup. To follow this guide you should be familiar with C#, NUnit and the principles of unit testing.

All code shown here will be available on my scripts repository on Github. I’ll be taking a few example functions from one of my scripts, and writing some simple stubbed tests to validate the functionality. All you really need to do to get set up is install NUnit via the MonoDevelop Add-ins manager.

Testing best practices

First of all, I’m going to assume that you, like myself, are unfamiliar with the Space Engineers codebase, and are retro-fitting tests to an existing script. If this is not the case, I would highly suggest practicing Test-Driven-Development, and writing your tests before starting on the actual script. Unfortunately, these scripts often require a certain amount of experimentation which can make this difficult.

It can be hard to figure out where to start adding tests, whether to test the “Main” function, or independently test every other function. Testing from the “Main” function becomes increasingly important as more code is added to it, if you have a simple “Main” function with only a few lines tying together a few abstracted functions, there isn’t much point in testing it. That being said it may not be worth testing your “Main” function without any internal stubs. If you only stub the GridTerminalSystem interaction the setup required to get your script operational from the “Main” functional all the way down to the lowest level may be considerable.

Since we know this code will be run in a restricted environment where the only point of entry will be the “Main” function, it’s fairly safe to overexpose functions for the sake of testing. It’s also safe to explicitly keep some functions private if the testing of these functions can simply be rolled into the parent function. The approach I’ll be using is to test functions in isolation, and stub up to a level beneath the code, even if it references another internal function, in order to simplify the test setup. Any complexity in testing setup for functions will be reduced by stubbing functions the code is calling.

I’ll be creating manual stubs for my code, however there are a number of decent stubbing and mocking libraries out there for C#. I’ll also be sticking to the principle of having one Assertion per test, and using initializers to set up testing conditions and variables.

Pure code example

The first function I’ll be tackling is this one

This code is used to translate the stored power/capacity, or discharge/charge rates from power blocks (solar, battery, reactor etc) into a numerical value. It takes a string from the details page, such as “100 kWh” and translates that to a W/Wh unit (100,000).

Since this code does not interact with the grid system, or call any other internal functions it’ll be easy to test. We can simply pass in a wide range of strings to trigger the various conditions; blank, invalid, W, Wh, kW, kWh etc, and validate the returned number matches. Here are a couple of tests, note, I’m creating powerControl in an initialization step.

Before we run these tests we need to change the function access modifier for getPowerAsInt. By default it is private, setting it to internal lets us access it from within the namespace, without making it entirely public. Now lets run some tests!

Oh dear.. it turns out this code does not handle error cases well. Lets if we can’t refactor it a little, check out the updated version which passes these tests on Github. That was a simple function greatly improved and made considerably less brittle through tests.

TerminalBlock example

Functions that interact with the Space Engineers core types can be a little harder to test. Luckily the developers created just about everything as an interface, so it’s trivial for us to create a stub that matches the interface, and then use it in a test. Both MonoDevelop and Visual Studio have the ability to generate stubs from defined interfaces, so we can start with that. To generate a stub from an interface in MonoDevelop, create a class extending the interface, click inside the class braces, and press ALT + Enter. This will bring up the code generation window.

Select all the implemented interface members, and then press Enter, this will fill out your stub class with functions that throw exceptions. This new class will match the interface IMyTerminalBlock, and as we need to use it, we can extend it and override specific functions on temporary, test specific classes. The end result should look something like this.

Anything we wish to override, we’ll have to add the virtual keyword to the function definition. Lets take a look at the code we’ll be writing tests for:

This code pulls the DetailedInfo field from the block, splits it by line, converts each line to a keypair, and pulls the value for the key specified. This is useful for looking up attributes not exposed by the standard ModApi. Lets write some tests!

For these tests I’ve set the block details to be a diverse string with some valid keys, some invalid:

a: b

c :d\r e : f\r

foo

bar:baz:bing

The block object is custom instance of the IMyTerminalBlock stub we created earlier:

This stub accepts the details through a constructor, and stores them, returning them via “block.DetailedInfo”. Note we override only the details accessor, nothing else. This allows us to catch unexpected behavior, such as a section of our code making a call we don’t expect. With the existing stub, that call will throw an exception that we’ll be able to see in the tests.

After running some tests it’s clear our code is brittle, it doesn’t respond well to spaces or error paths. Refactoring produces something a little better:

GridTerminalSystem example

The final example is a little simpler so I’ll be brief. The code we’re testing here pulls all blocks of a certain type and returns them as a list. It’s basically an accessor on top of GridTerminalSystem.GetBlocksOfType.

There won’t be much we can fix up here, but it’s still important to test it. Once again we’ll have a custom stub we can use to return a specific list of objects, and we can test that getBatteries returns what we expect:

In one test we’re initializing our stub to return a list with one block, and in the other its returning an empty list. We simply need to assert that getBatteries returns the block list we specify in the stub. This works even though getBatteries is creating a new list to return, since the list are still equal in content. Lets take a look at our stub:

Just like before we’re extending the base stub, and overriding the necessary functions with the logic we need for the test. In this case we’re ignoring the collect function since our code does not use it, and we’re filling the blockList provided with the blocks injected via the constructor. Something to note is that the type check is taking place. This validates the fact that our code is calling GetBlocksOfType with the correct generic type as a side effect. Should our code call with a different type it, the stub would pass back an empty list causing a failure in the happy path test.

Conclusions

NUnit seems to work fairly well for testing scripts, I want to explore mocking/stubbing libraries more, and see if I can directly create instances of the terminal blocks Space Engineers uses rather than stubbing. The isolation stubbing is a benefit however, changes to the implementations could break script tests in the future, however the interfaces should remain consistent.

Hopefully this guide has been helpful. Check out my SpaceEngineersScripts repository on Github for the full code listing.