There’s no fire without Smoke Tests

Tickets to my next automation workshop are on sale!

It’s a familiar tale: you’ve set up an expensive and time-consuming playtest, and just wanted to squeeze in one more feature. You cook the build, give it a quick sanity check, and send it off. The playtest starts well, but as players start to hit level 4, they encounter bugs, error spam and fatal crashes. Your final feature caused a knock-on effect deep into the game, and now your playtest is ruined.

Wouldn’t it be great if there was a type of automated test that could quickly validate the whole project? It might not find specific issues, but if there’s a fire, it’ll spot the smoke, prompting you to investigate more.

Flirtation by Andrew Yee, CC BY 2.0

What are Smoke Tests?

A smoke test is an automated test that “explores as much of the game runtime as is reasonable, looking for obvious problems”. It’s not far off the type of test people imagine when first learning about test automation: a system that plays the game from start to finish looking for issues. You might be lucky in that your game already has bots or some similar player-facing feature which can be repurposed for a full playthrough, but you can still have a very effective smoke test suite without it.

In fact an effective smoke test can be as simple as loading each level in turn, waiting, and returning to the main menu. This may not exercise every gameplay system, but a passing test still gives you a lot of confidence about the game: at least the level exists, at least it loads without problems.

What do we mean by “problems”? A great place to start is checking the game’s log for exceptions, errors and warnings. Just by doing that you can catch problems such as:

  • Level 5 missing a critical gameplay object
  • Initialisation order bugs between systems on level 8
  • Incorrect assumptions about world state in level 12
  • …and a whole load of validation that the game engine will already be doing for you to make sure the core of the game is running correctly

When a smoke test fails, often all you’re really learning is that there’s a fire somewhere, but not what that fire is. We’ll normally have to do some investigating to see what caused the failure, but it’s still an incredibly useful safety net.

And it’s extremely effective for the amount of effort it takes. By the end of this blog post you’ll see how small a real smoke test suite can be – considering the value you get out of them, they’re maybe the tests with the best bang-for-buck out there.

Creating Smoke Tests (In Unity)

As an example, we’ll implement a smoke test in Unity. I’m going to assume you’re already familiar with Unity Test Framework and NUnit, although the general principles here apply to any game engine or test framework.

Let’s build the smoke tests in Unity’s Karting demo. I like using this because it’s not designed to be automated, but we can still do a lot of stuff very quickly. For those not familiar, below is a user playing the game through the main menu, past the race countdown, and starting to race. It doesn’t seem unreasonable to simulate that in our smoke tests.

A main menu showing PLAY, a mouse pointer selects the button. A kart track loads, and counts in 3 2 1 before the kart drives off.

We’ll break this down into multiple individual tests:

  • Can we boot to the main menu without problems?
  • Can we load the track without problems?
  • Does the race start without problems?

We’ll create a LevelSmokeTests.cs in an appropriate PlayMode folder, but for now it just has a blank first test:

using System.Collections;
using UnityEngine;
using UnityEngine.TestTools;

public class LevelSmokeTestsFixture
{
    [UnityTest]
    public IEnumerator MainMenu_Booted_NoErrors()
    {
        yield return new WaitForSeconds(10); // just to get it compiling
    }
}

(We’re using the GIVEN_WHEN_THEN naming convention here)

You’ll notice when you run this test, that the game boots to an empty level and does nothing, rather than booting to our main menu:

A running empty unity playmode test, showing an empty Hierarchy and blank scene view.

This is how all playmode tests behave. It’s up to you to get us to the next step. In our case, we want to load scene 0 in our build list and wait for the menu to load. We’ll test for problems the same way every time – subscribing to Application.logMessageRecieved, and checking for error type. We’ll set a flag to true if we see a problematic log. This lets us also fail the test if we see any warnings — I strongly believe warnings should be treated as seriously as errors, but that’s another blog post.

public class LevelSmokeTestsFixture
{
    [OneTimeSetUp]
    public void LevelSmokeTestsOneTimeSetup()
    {
        Application.logMessageReceived += OnLog;
    }

    [SetUp]
    public void SetUp()
    {
        m_isTestFailed = false;
    }
    
    private void OnLog(string condition, string stacktrace, LogType type)
    {
        if (type is LogType.Assert or LogType.Error or
                    LogType.Exception or LogType.Warning)
        {
            m_isTestFailed = true;
        }
    }
    private bool m_isTestFailed;
    
    [UnityTest]
    public IEnumerator MainMenu_Booted_NoErrors()
    {
        SceneManager.LoadScene(0);
        yield return null;
        Assert.That(m_isTestFailed, Is.False);
    }
}

Note that this is not a complicated project, so we can rely on yield return null to fully get us to the main menu, but if you use a loading screen, you might need to wait until there’s some obvious signal that the game is booted – maybe the presence of a specific gameobject or singleton.

We can now run the test and maybe catch the main menu appear in the Game window for a brief moment before the test exits. Congratulations, that’s a decent smoke test! Just having this running on every build will catch a huge category of bugs. You could stop here and it would be fine, but let’s move a little further.

Testing our Test

I said we were done with that test, but there’s actually an important step to remember: testing the test. It’s very easy to write a test that passes but isn’t actually testing what you think it is. Whenever you write a new test, you should always see it fail for a predictable reason first. To do that, we’ll just add a Debug.LogError to somewhere in the boot code – I chose an Awake function in LoadSceneButton – and run the test to see it fail.

A unity Test Runner window, showing our test in red, with text below saying there was an unhandled log message.

Now we know the test works, we can commit it to source control and move on.

Avoiding UI Testing In Smoke Tests

Our next step is to hit the big Play button in the main menu. It’s tempting at this point to force a mouse click or something – after all, that’s most representative of the player’s experience, and if the Play button is broken, we’d want to know in a smoke test, right?

I always recommend people new to testing avoid the UI as much as possible. If we did a mouse click here, or even something slightly more intelligent like find the gameobject by name or tag and force a click event, then suddenly the way this test will be most likely to fail is because someone moved the button. Or renamed the button. Or changed the hierarchy.

Those aren’t true test failures, in testing lingo they would be “false positives” – and in general false positives undermine trust in the automation. It’s easy to see a failure and assume it’s just a UI change again. Not only that, but any failure caused by changed UI means updating the test, which is easy to deprioritise. Pretty soon the boot test has been failing for weeks, and it might as well not exist.

On top of that, if your Play button is genuinely broken, QA will definitely notice it very quickly. We’re not helping QA to focus here.

So while there are techniques to do this more robustly, it’s best when starting out to avoid the UI layer entirely. Find the thing that the button pokes and call that from our test. How your level loads is much less likely to change over the lifetime of a project than the UI layout is.

In our case, the button calls SceneManager.LoadSceneAsync directly, so that’s what we’ll call in our test:

[UnityTest]
public IEnumerator MainMenu_AndLoadsLevel_NoErrors()
{
    yield return SceneManager.LoadSceneAsync("MainScene");
    yield return null;
    Assert.That(m_isTestFailed, Is.False);
}

However if we run this and pay close attention, we see that this new test runs before our boot test! That’s not right. In fact, NUnit does not define an execution order for tests (although in practice it’s alphabetical), because it’s normally a good idea to not have dependencies between tests. Dependencies can lead to fragile and hard-to-maintain tests, but in the case of most smoke tests, we need them. We’re looking to move through the game in stages, and it’s important to force an ordering here.

Luckily, the [Order] attribute exists just for that. Below I’ve added it, and also renamed the tests slightly so the alphabetical display in the editor reflects our ordering:

[UnityTest,Order(00)]
public IEnumerator T00_MainMenu_Booted_NoErrors()
{
    SceneManager.LoadScene(0);
    yield return null;
    Assert.That(m_isTestFailed, Is.False);
}

[UnityTest,Order(10)]
public IEnumerator T10_MainMenu_AndLoadsLevel_NoErrors()
{
    yield return SceneManager.LoadSceneAsync("MainScene");
    yield return null;
    Assert.That(m_isTestFailed, Is.False);
}

Now when you run both tests, you might be able to catch them execute in order. You’ll notice the order is increasing in chunks of 10, so if there’s a future need, it’s not too disruptive to add tests in-between.

Now this test is complete, don’t forget to test your test and add an error somewhere in the game code for it to catch.

Waiting For Gameplay

Our last test is to wait for race start, and again check for problems in the Log. This is the first time we’ve had to make a gameplay-side code change, because TimeManager did not expose IsRaceStarted. It’s not normally a good idea to make gameplay-side changes just for tests, but a read-only property like this seems like it could be useful for future gameplay code, so it’s forgivable.

[UnityTest, Order(20)]
public IEnumerator T20_Race_Started_NoErrors()
{
    var timeManager = Object.FindFirstObjectByType<TimeManager>();
    yield return new WaitUntil(() => timeManager.IsRaceStarted);
    yield return new WaitForSeconds(0.5f);
    Assert.That(m_isTestFailed, Is.False);
}

It’s perfectly valid to stop here, or to extend the smoke tests further over time.

The final successful test run might look something like this:

A full smoke test run, launched from the unity test runner. It loads the menu, enters the track, waits for the countdown, then exits the game with three green ticks.

Dealing With Smoke Test Failures

While it’s great that these tests are giving us a safety net, they’re not perfect. They can be slow compared to other kinds of automated test, and they are not always very informative. “There was an error while loading level 3” is useful but only the start of a journey.

That’s why it’s good to ask yourself when fixing smoke test failures if there’s a way to construct a faster, more informative automated test. A fire test to go with the smoke test. That way, if this particular problem happens again, you’ll be told more quickly and in more informative terms. That could take the form of an asset test, unit test, actor test or something else.

This can be a great way to create pragmatic useful tests for your project over time. If you follow this pattern, you’ll initially see a lot of smoke test failures, but as your project progresses, you’ll see less and less and fire test failures will take over.

More Like This

I hope you see that this was not complicated to add, to a project that wasn’t designed for automation, and consisted of basically two three-line functions and a four-line function. Adding smoke tests to your project should not be a big investment.

If you’d like to learn more about smoke tests and other test types, I often work with studios to help them get started with test automation through training and workshops. I also sometimes host public-facing workshops that you can buy individual tickets for. Find out more details at automateyour.games.

This Graph Doesn’t Exist: The Ghost of The Delayed Issue Effect

So this graph is pretty famous, especially in quality circles. It’s meant to convey that fixing things earlier is better – by orders of magnitude. It’s a pretty stark visual of something we feel is intrinsically true.

Only problem is, it’s bullshit.

This specific visual is from The Journal Of Information Systems Technology And Planning by Dawson et al, and they reference it from another source, saying “A study was performed by the IBM System Science Institute”. There’s no citation for this study in the paper. Googling around, there’s very few references to the IBM System Science Institute at all. What’s going on?

This is not the first paper to use this 1/6.5/15/100x data, and Morendil on github has done the deep dive, but it turns out it was probably someone’s gut feel for a course taught internally at IBM. Not a study, not a published paper, just numbers pulled out of someone’s ass. Through a game of telephone and poor scholarship it’s become quoted as absolute truth.

So is there any real evidence for this?

Menzies et al did a study in 2016 that seems to be the only real attempt to figure out if delaying fixes is more costly (they call it the Delayed Issue Effect, DIE) with real data, and they find no conclusive evidence.

However they were working with projects with median duration of 60 days and programs only 4,000 lines long. It’s not surprising that any DIE wouldn’t manifest itself at this scale, so I don’t think it’s conclusive.

We might never have good evidence here! Large-scale computer science research seems like a really hard problem to crack. The data just isn’t shared from commercial development. But that doesn’t mean we should perpetuate bad graphs just because they correlate with our gut feel.

If anyone has any better data on the Delayed Issue Effect, let me know!

Exploding barrels, and why your first test should be an Asset Test

On Rollerdrome one day, we had a p2 bug come in where a specific red barrel on level 3 wasn’t exploding when shot. As we all know, it’s not really a video game unless barrels explode – it’s just a sparkling spreadsheet – so this was important to fix.

(Exploding barrels from Half Life 2, because Rollerdrome’s barrels are actually quite hard to screenshot)

When a programmer looked at the bug, it became obvious that this was a simple fix. Someone had accidentally changed the layer on this barrel, meaning bullets couldn’t hit it and the barrel wouldn’t explode. This is a dream bug for a programmer! Reset the dropdown, submit the change, resolve the ticket, move on.

However from a QA point of view, it’s a very scary bug. If it’s possible that one barrel has the wrong layer, maybe more do? Maybe we should explode every barrel in the game, to double-check? Maybe we should do that before every release? Their checklist has just grown a little bit with a task that’s repetitive and error-prone.

But what if we could add some automation to stop this bug from ever coming back, and allow QA to look at more interesting issues and repros? What if it was the perfect kind of test for people new to testing?

Asset Tests

An asset test is any test that looks at data on disk – blueprint configuration values, prefab variables, texture compression values, static mesh properties, etc. Asset tests answer questions like:

  • Do all animations follow the correct naming conventions?
  • Does every gun have a valid ammo type assigned?
  • Is every barrel on every level set to the correct physics layer?

Because these tests stay away from gameplay logic of any kind, they are normally very simple to write – almost always just a line or two.

They’re also very simple to maintain, because “barrels being on the correct physics layer” is very unlikely to change for the lifetime of the project.

On Rollerdrome, by quickly writing a new asset test for barrels in levels, we made sure the build server never generated a build with barrels on the wrong layer, and saved QA the time to explode every single one, for every single major release. If writing the test took 30 minutes (which is an overestimate), we wouldn’t have to go through many major releases to start making a return on that investment.

Implementing Asset Tests

This isn’t a full tutorial for asset tests (let me know if you want one of those) but we’ll discuss some of the prerequisites:

  • A test framework
  • A build server
  • (Highly desirable) Parameterised test fixtures

Unity has Unity Test Framework, which is a wrapper around NUnit, a populate C# test framework. Unreal has many testing frameworks, but I think Spec is particularly well suited to asset tests.

You should have a build server of some kind, and it should run your tests, which in most cases will just be a specific command line call to your engine. It’s important it reports test failures in an obvious way, and how it does this is framework-dependent. Unty’s DevOps build system and Epic’s Gauntlet can both do this for you.

Parameterised Test Fixtures

A “test fixture” is a group of related tests. In NUnit/Unity Test Framework, this looks like a class with an attribute containing a number of public functions. If we’re writing a scene asset test fixture, to check many things alongside barrel layers, that might look like this:

using NUnit.Framework;

namespace Game.Tests {

  [TestFixture]
  class SceneAssetTestFixture {

    [Test]
    public void AllBarrelsOnCorrectLayer() {
      var allBarrels = 
          Object.FindObjectsByType<Barrel>(FindObjectsSortMode.None);
      foreach (var barrel in allBarrels)
      {
        Assert.That(barrel.gameObject.layer,         
            Is.EqualTo(GameConstants.BulletLayer));
      }
    }

    // more scene asset tests...
  }
}

This is great, but how do we test this against every scene? We could put code inside our AllBarrelsOnCorrectLayer test to load every scene in turn, but then a failure will be less than informative, and if we added more tests for other things in a scene, we’d have to duplicate that logic.

Instead we can use an NUnit feature called TestFixtureSource to create a “parameterised” test fixture – letting NUnit create one fixture per level for us. We’ll use [OneTimeSetUp] here to load the scene only when the tests start. Similar features exist for other test frameworks.

using NUnit.Framework;

namespace Game.Tests {
  public class SceneSource : IEnumerable<string> {
    public IEnumerator<string> GetEnumerator() {
      return EditorBuildSettings.scenes
          .Select(scene => scene.path)
          .GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
  }

  [TestFixtureSource(typeof(SceneSource))]
  public class SceneAssetTestFixture {
    private readonly string m_sceneName;
    public SceneAssetTestFixture(string sceneName) {
      m_sceneName = sceneName;
    }

    [OneTimeSetUp]
    public void OneTimeSetup() {
      EditorSceneManager.OpenScene(m_sceneName, OpenSceneMode.Single);
    }

    [Test]
    public void AllBarrelsOnCorrectLayer() {
      // ...

To explain this code:

  • SceneSource produces a sequence of strings, each of which is the path of a scene, coming from the build list. This logic can be customised for your project.
  • SceneAssetTestFixture now has a [TestFixtureSource(typeof(SceneSource))] and has a string constructor
  • One SceneAssetTestFixture will be instantiated for every scene
  • When we run the tests, OneTimeSetUp runs, which loads the scene in the editor
  • …the AllBarrelsOnCorrectLayer, and any other scene asset tests, can now run

In the unity test runner, you see one entry per scene, so can easily see which scenes have problems, and easily run specific tests on specific scenes.

When you run these, you’ll be able to see Unity load each scene in turn, quickly run the tests, and return to the scene you’re currently editing. It’s not instant, but it’s still very swift.

We had to write a little boilerplate for the first test, but we’re talking an hour at most, and then adding new tests should be really straightforward. Similar setups could be made for, for example, every prefab with a specific tag or asset under a certain folder.

Driving new asset tests through bugs

Now you know how to do this, it’s tempting to firehose in new tests of any and everything you can think of. This can work, but if you write a test and it never fails for the lifetime of the project, you’ve wasted time writing it and repeatedly running it.

Better to write tests that have a plausible chance of failing, and a good source of those is Regression Tests. Regression tests are tests you put in with a bug fix, to make sure the bug never comes back. They definitely have plausibility, because you’ve seen it happen once, and you can very quickly grow a large suite of useful tests this way.

Not all bugs are suitable to be turned into an asset test, especially if they involve game logic, but a surprising number are.

Asset tests as a gateway, or as a destination

I hope you now see that asset tests can very easy to set up, are so simple they don’t need much maintenance, and can bring concrete benefits very quickly to a project. You could just do this, and do no other automation in your project, and you’ll have done a good thing.

However if you want to go further, you should check out my Develop 2023 talk about Rollerdrome’s pragmatic approach to test automation for what to do next, and/or contact me on LinkedIn to chat about specialist training!

The Topography of Unreal Test Automation in 2025

I’ve been exploring automated testing more in Unreal recently, and one thing that won’t surprise Unreal users is there’s a million different ways to do it, each with their pros and cons, and half of them are undocumented. I thought I could do a useful overview of the different automated testing tools at your disposal in Unreal 5.5.

This isn’t a tutorial, it’s not even a fully-fleshed out blog post, but enough for you to get a feel for things and start your own research.

Map Check

An easy-to-extend tool for validating the setup of an individual map at edit or build time. Run via Build > Map Check.

If you have custom actors that can validate their own internals, you can add to map check by overriding CheckForErrors(). For instance, making sure weapon pickups are on the correct layer.

You could also create a custom map check actor whose only job is to find other actors in the scene and validate their setup – for instance, making sure there’s only one player spawn point, or that no static mesh actor has missing materials.

Map checks don’t run on save, so it can be easy to work on a scene and commit it in a broken state. They’re also very coarse – you run all map checks in a level, or none. There’s no popups on running the map check from the menu, output is just sent to the Message Log, making it easy to miss.

Asset Data Validation

A more complicated mechanism than Map Check, but extends edit-time validation to all UObjects. Some actual documentation exists for this: https://dev.epicgames.com/documentation/en-us/unreal-engine/data-validation-in-unreal-engine

You can either validate an entire project through Tools > Validate Data, or you can right-click on individual assets/folders in the content browser to validate them (Asset Actions > Validate Assets).

You can add validation to a custom UObject that overrides IsDataValid() to validate itself. For instance, a Data Asset could make sure that its parameters have sensible values.

You can also create a UEditorValidatorBase responsible for finding other UObjects and validating them. One obvious synergy with Map Check is to create a validator base that finds all levels and makes sure they have exactly 1 custom map check actor.

Asset Data Validation gives you a popup while it’s working, and sends output to the Asset Check tab of the Message Log. It runs on save, and things like Data Assets give you a nice visual warning that they’re in a bad state.

Automation Framework

The automation framework is the base runtime for a few different automation systems. You’ll access all of them through Tools > Session Frontend > Automation.

This is an annoying window for the local automated tester! It’s designed for running large-scale tests across multiple machines, and the workflow of wanting to iterate on some local tests before committing is not really considered. Larger projects should consider writing their own simple frontend.

I at least recommend you put your project’s tests in a folder named _AA or something, to make sure they’re at the top of the list!

Automation Framework: Automation Tests

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FPlaceholderTest, "TestGroup.TestSubgroup.Placeholder Test", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)

bool FPlaceholderTest::RunTest(const FString& Parameters)
{
	// Make the test pass by returning true, or fail by returning false.
	return true;
}

This is the base layer runtime – everything else just provides wrappers around automation tests. It’s a C++ macro-heavy library that won’t be too unfamiliar to people who have used something like googletest, but it has some severe limitations, not least of which is the lack of before/after helper functions.

You can however write some form of parameterised tests, using the Complex Test helpers. This is very handy for asset validation.

To write multi-frame tests, you can use the Latent Command system to essentially queue lambdas, however it is quite verbose.

In my experience I think it’s best to understand automation tests, but then mostly use one of the other frameworks.

Automation Framework: Functional Tests (Blueprint)

This library allows you to write tests in blueprint. “Functional” is in its name, but there’s not much stopping you from writing low-level unit tests in this – the only factor is, can you access what you need from blueprint?

Jessica Baker’s Unreal Fest talk about automation in Sea of Thieves is a great starting point not only for people interested in the practice, but also anyone who wants to know more about the Functional Tests library, and how Rare use it to write Actor Tests.

It’s also straightforward to use functional blueprint (or even python) tests to write editor tests. People often forget about testing your tools!

Automation Framework: Spec

BEGIN_DEFINE_SPEC(MyCustomSpec, "MyGame.MyCustomClass", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask)
	TSharedPtr<FMyCustomClass> CustomClass;
END_DEFINE_SPEC(MyCustomSpec)
void MyCustomSpec::Define()
{
	Describe("Execute()", [this]()
	{
		It("should return true when successful", [this]()
		{
			TestTrue("Execute", CustomClass->Execute());
		});
		It("should return false when unsuccessful", [this]()
		{
			TestFalse("Execute", CustomClass->Execute());
		});
	});
}

Spec is a BDD-focused library, but if that acronym means nothing to you, the only thing you have to contend with is the slightly odd naming scheme and the way tests can nest.

The advantages of Spec is that it’s relatively easy to write parametric tests, and that you can use a proper fixture with BeforeEach/AfterEach functions. It is also somewhat easier to write asynchronous tests, either in a pool or by building latent actions, but it’s still far from fluent.

The big drawback of Spec in my eyes is that there’s no BeforeAll/AfterAll! You can work around this in the BeforeEach to make sure (eg) you only load an asset if it’s not currently loaded, but I have not yet found a good way to know if it’s safe to skip unloading in the AfterEach.

I like to use Spec to write asset tests, rather than the Asset Data Validation system, so I have more control over what tests are running, and get more useful information in the output. I would even use it for validating maps, as it’s relatively straightforward

Automation Framework: CQTest

TEST_CLASS(LatentActionTest, "Game.Test") 
{
	uint32 calls = 0;

	BEFORE_EACH() 
	{
		AddCommand(new FExecute([&]() { calls++; }));
	}

	AFTER_EACH() 
	{
		AddCommand(new FExecute([&]() { calls++; })); // executed after the next line, as it is a latent action
		ASSERT_THAT(AreEqual(2, calls));
	}

	TEST_METHOD(PerformLatentAction) 
	{
		ASSERT_THAT(AreEqual(1, calls));
		AddCommand(new FExecute([&]() { calls++; }));
	}
};

CQTest is relatively new, and is an upstream merge of the system Rare built for Sea of Thieves. It’s a much more accessible general c++ testing library than the base automation system, including fixtures, before/after each/all, and even better access to running latent asynchronous commands through the command builder. It also comes with a suite of helpers to help you enter PIE and spawn objects.

It’s very good for writing gameplay tests of all scopes, from unit to functional, but two things are holding it back right now:

Firstly the documentation is early and limited. In fact, the best documentation is the Lyra starter project, which has a readme and lots of examples. In fact, I often find the best way to learn about unreal features of any kind is a GitHub search to view example code.

Secondly, I’m yet to figure out a way to do parametric tests with CQTest. Which means it’s unsuitable for asset tests or fuzz testing or many other situations. It’s a shame, because if you could have the parametric tests from Spec, and the Before/AfterAll from CQTest, you’d have the perfect library for building unreal asset tests!

Yet More Systems

Gauntlet is a system for orchestrating tests across multiple runs, which seems particularly suitable for multiplayer games. I’ve not used it much in anger to comment about it.

Low Level Tests are a new paradigm in Unreal, built upon Catch2, a general-purpose open source C++ test library, is now integrated in Unreal 5. I’ve used Catch2 in c++ projects and like it, but not sure yet where it fits into the Unreal ecosystem.

I hope you find this overview useful – if there’s things I’ve overlooked, let me know in the comments!

Setting up a Windows cloud machine to run unity with graphics, so Jenkins can run automated tests

The latest in a series of posts on getting Unity Test Framework tests running in jenkins in the cloud, because this seems entirely undocumented.

To recap, we had got as far as running our playmode tests on the cloud machine, but we had to use the -batchmode -nographics command line parameters. If we don’t, we get tons of errors about non-interactive window sessions. But if we do, we can no longer rely on animation, physics, or some coroutines during our tests! This limits us to basic lifecycle and validation tests, which isn’t great.

We need our cloud machine to pretend there’s a monitor attached, so unity can run its renderer and physics.

First, we’re going to need to make sure we have enough grunt in our cloud machine to run the game at a solid frametate. We use ec2, with the g4dn.xlarge machine (which has a decent GPU) and the https://aws.amazon.com/marketplace/pp/prodview-xrrke4dwueqv6?ref=cns_srchrow#pdp-overview ami, which pre-installs the right GPU drivers.

To do this, we’re going to set up a non-admin windows account on our cloud machine (because that’s just good practice), get it to auto-login on boot and ask it to connect to jenkins under this account. Read on for more details.

First, set up your new windows account by remoting into the admin account of the cloud machine:

  • type “add user” in the windows start menu to get started adding your user. I call mine simply “jenkins”. Remember to save the password somewhere safe!
  • We need to be able to remote into the new user, so go to System Properties, and on the Remote tab click Select Users, and add your jenkins user
  • if jenkins has already run on this machine, you’ll want to give the new jenkins user rights to modify the c:\Workspace folder
  • You’ll also want to go into the Services app, find the jenkins service, and disable it.
  • Next, download autologon https://docs.microsoft.com/en-us/sysinternals/downloads/autologon, uncompress it somewhere sensible, then run it.
    • enter your new jenkins account details
    • click Enable
    • close the dialog

Now, log out of the admin account, and you should be able to remote desktop into the new account using the credentials you saved.

Now we need to make this new account register the computer with your jenkins server once it comes online. More details here https://wiki.jenkins.io/display/JENKINS/Distributed+builds#Distributedbuilds-Agenttomasterconnections, and it may be a bit different for you depending on setup, but here’s what we do:

  • From the remote desktop of the jenkins user account, open a browser and log into your jenkins server
  • Go to the node page for your new machine, and configure the Launch Type to be Launch Agent By Connecting It To The Master
  • Switch to the node’s status tab and you should have an orange button to download the agent jnlp file
  • Put this file in the %userprofile%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup folder
  • Change the Launch Type back to whatever you need (we use the Slave Setup plugin, despite the icky name https://plugins.jenkins.io/slave-setup/) — it doesn’t need to stay as Launch Agent By Connecting It To The Master.

We’re done. log out of remote desktop and reboot the machine. You should see it come alive in the jenkins server after a few minutes. If you remove the -batchmode and -nographics options from your unity commands, you should see the tests start to run with full physics and animation!

Building Unity using Jenkins Pipelines

This is the third of a series of posts on Jenkins and Unity.

In this post I’ll outline how I set up Jenkins to make reliable repeatable Unity builds, using a pipelines script (also called a jenkinsfile) that’s stored inside source control. I’ll outline the plugins I used and the reasons behind some of my choices. This is not a tutorial in Jenkins or pipeline scripts. It’s more of a tour.

I’m not an expert in Jenkins – most of this is pieced together from the Pipeine docs and google. It may be that there are better ways to achieve the same results – I’d be keen to hear about them in the comments!

This post just deals with the jenkinsfile. In a future post I’ll deal with how I configured Jenkins to use it.

I am using Jenkins v2.257.

Why Pipelines?

You can set up Jenkins to do almost anything you want via the web interface. This is ok for experimenting, but it has drawbacks:

  • config changes aren’t easy to document
  • nor can they be rolled back
  • it’s hard to share common build setups between related Jenkins jobs

All these go away if we move to a piepelines script stored in a file (commonly called “the jenkinsfile”) within the project’s source control.

Supported jobs

In Jenkins, a “job” is a particular way of building your project. This one jenkinsfile will run multiple jobs for us:

  • VCS Push: every time anyone pushes to SVN, Jenkins will pull latest, run some quick automated tests, and make a build. If any step fails, Jenkins reports a failure. The build artifact is thrown away.
  • Health Check: on a schedule, multiple times a day, Jenkins will do some more time-consuming automated tests, and make a build. If any step fails, Jenkins reports a failure. The build artifact is thrown away.
  • Deploy: at 6am and 12pm, do everything Health Check does, but make the result available to QA. They’ll then smoke the build further and make it available to everyone else if it’s good.

The Health Check build exists because there’s a big gap between our 12pm build and the next day’s 6am build. If someone commits something at 1pm that would fail the slow automated tests, we might not know until the next morning, and the QA build will already be late. Now the Health Check build runs multiple times in the afternoon, and we can fix stuff before we log off for the day.

File structure

When you’re googling for pipelines info, you’ll discover it’s a concept that has undergone a few revisions. What we’re talking about here is a “declarative pipeline” (as opposed to the older “scripted pipeline”), which is (mostly) composed of a jenkinsfile written in a subset of the Groovy language, which runs on the JVM.

A declarative pipelines script is roughly defined as a series of “stages”, where each stage is a series of commands, or “steps”. Stages can be hierarchical and nest and run in parallel, but for our purposes, we’re going to stay pretty linear and flat. If any stage fails, the subsequent stages won’t run.

We’re going to have six stages:

  • Clean – we’ve all had corrupted library files, and sometimes you want to make really sure you’re starting from scratch.
  • Prewarm – getting things ready so we can start the builds
  • Editmode Tests – fast declarative-style tests running under the Unity Test Framework
  • Playmode Tests – slower tests that require running the game. See my recent post about running playmode tests on Jenkins.
  • Build – we always care if this succeeds, but depending on the job we may throw away any produced artifact. We want to capture those #if UNITY_EDITOR errors!
  • Deploy – putting the build artifact somewhere useful. This is somewhat project-dependent and, at the moment, confidential, so I will only give the briefest outline of this step.

Metadata

You’ll find the full jenkinsfile at the bottom of the page, but let’s break it down by starting at the top.

The pipeline itself doesn’t start until the pipeine keyword, but since this is a subset of Groovy, we can define constants at the top.

UNITY_PATH = "C:\\Program Files\\Unity\\Hub\\Editor\\2019.4.5f1\\Editor\\Unity.exe"

There’s probably a better service-orientated way of installing and locating Unity, but in this case we chose to just manually create a windows machine on ec2, remote desktop in, and install Unity. Ship it.

Next we start the pipeline, and define parameters. These appear as dropdowns when starting a job in Jenkins, and you access them further down the script using params.PARAMETER_NAME:

pipeline {
    parameters {
        choice(name: 'TYPE', choices: ['Debug', 'Release', 'Publisher'], description: 'Do you want cheats or speed?')
        choice(name: 'BUILD', choices: ['Fast', 'Deploy', 'HealthCheck'], description: 'Fast builds run minimal tests and make a build. HealthCheck builds run more automated tests and are slower. Deploy builds are HealthChecks + a deploy.')
        booleanParam(name: 'CLEAN', defaultValue: false, description: 'Tick to removed cached files - will take an eon')
        booleanParam(name: 'SKIP_PLAYMODE_TESTS', defaultValue: false, description: 'In an emergency, to allow Deploy builds to work with a failing playmode test')
    }

Here’s how they appear in Jenkins in the Build With Parameters tab:

More details on each param:

  • Type: we’ll pass this option directly to our Unity build function, which will set various scripting defines as required.
  • Build: this is used only within the jenkinsfile, to switch different parts of the pipeline in and out. It correlates with the job types mentioned above.
  • Clean: sometimes you gotta nuke the site from orbit
  • Skip Playmode Tests: a nod to practicality – I love automated tests but very rarely you know the test failure is manageable, and you need a build right now. In practice our team rarely uses this option.

Here follows some boilerplate for Jenkins to understand how to run our job:

agent {
    node {
        label "My Project"
        // force everyone to the space space, so we can share a library file.
        customWorkspace 'workspace\\MyProject'
    }
}
options {
    timestamps()
    // as a failsafe. our build tend around the 15min mark, so 45 would be excessive.
    timeout(time: 45, unit: 'MINUTES')
}

The node block is about finding an ec2 instance to run the job on. We’ll deal with ec2 setup in a future post. The customWorkspace setting is a cost-saving measure: on ec2, the size of persistent SSD storage is a significant part of our costs. We could save money by switching to spinning rust, but we want the build speed of an SSD. Instead, we’ll try to keep SSD size down by not having multiple versions of the same project checked out all over the drive. In practice, we mostly only work in trunk anyway, and when we build another branch it’s not massively divergent. We may revisit this over the course of the project.

Clean

Our first stage! It’s pretty simple. It only runs if the Clean parameter has been set, and it just runs some dos commands to clean out the library and temp folders:

stages {
    stage('Clean') {
        when {
            expression { return params.CLEAN }
        }
        steps {
            bat "if exist Library (rmdir Library /s /q)"
            bat "if exist Temp (rmdir Temp /s /q)"
        }
    }

(I’m surprised that using the boolean Clean param in a when block isn’t easier? I may have missed some better syntax)

Prewarm

Prewarm helps set the stage for the coming attractions. We’re going to use some script blocks here to drop into the old scripted pipeline format, which lets us do some more complex logic.

The first thing we want to do is figure out what branch we’re on. Jenkins will always run your pipeline with some predefined environment variables, including some which seem to imply they’ll contain your branch name, but try as I might they didn’t seem to work for us. Maybe it’s because we’re using SVN? So I had to figure it out for myself:

stage('Prewarm') {
    steps {
        script {
            // not easy to get jenkins to tell us the current branch! none of the built-in envs seem to work?
            // let's just ask svn directly.
            def get_branch_script = 'svn info | select-string "Relative URL: \\^\\/(.*)" | %{\$_.Matches.Groups[1].Value}'
            env.R7_SVN_BRANCH = powershell(returnStdout: true, script:get_branch_script).trim()
        }

We’ll call out to powershell because urgh grepping in dos is esoteric. We’ll store it in a new env variable, which will let us use this info in future stages.

Next we’ll set up the build name:

buildName "${BUILD_NUMBER} - ${TYPE}@${env.R7_SVN_BRANCH}/${SVN_REVISION}"
script {
    if (params.BUILD == 'Deploy') {
        buildName "${env.BUILD_DISPLAY_NAME} ^Deploy"
    }
    if (params.BUILD == 'HealthCheck') {
        buildName "Health Check of ${TYPE}@${env.R7_SVN_BRANCH}/${SVN_REVISION}"
    }
    if (params.BUILD == 'HealthCheck' || params.BUILD == 'Deploy') {
        // let's warn that a deploy build is in progress
        slackSend color: 'good',
            message: ":hourglass: build ${env.BUILD_DISPLAY_NAME} STARTED (<${env.BUILD_URL}|Open>)",
            channel: "project_builds"
    }
}

the buildName command lets you set your build name, and env.BUILD_DISPLAY_NAME contains the current version of that. The default Jenkins build name is just “#99” or whatever, which is less than helpful. Here, we’ll make sure it’s of the format “JenkinsBuildNumber – Type@Branch/Changelist [^Deploy]”. It’ll then be obvious from a glance in both the Jenkins dash and slack notifications what’s building and why.

We also send a slack notification of health check and deploy builds, since it’s useful to know they’ve started. It gives people a good sense of if their commits have made it into the build or not. More on notifications below.

Next some more housekeeping for the automated tests, which communicate with Jenkins via xml files:

// clean tests
bat "if exist *tests.xml (del *tests.xml)"

Finally, we’ll open Unity once and close it. One persistent problem with Unity in automated systems is of serialisation errors from out-of-date code working with new data. For instance, let’s assume you’ve got a bunch of existing scriptable assets, and your latest commit refactors them. On the build server, Unity will open, validate the assets with the pre-refactor code that it has from the last run, throw some errors because nothing looks right, then rebuild the code. Subsequent launches will succeed because both the data and the code are in sync. So, to keep those spurious errors out of real build logs, we’ll do this kind of ghost-open:

retry(count: 2) {
    bat "\"${UNITY_PATH}\" -nographics -buildTarget Win64 -quit -batchmode -projectPath . -executeMethod MyBuildFunction -MyBuildType \"${params.TYPE}\" -MyTestOnly -logFile"
}

This is the first time we’ve seen Jenkinfile talk to Unity! We’ll explain more in the next section, but just pretend you understand it for now. The important part is -MyTestOnly, which tells our build function to only set script defines, recompile, and quit.

We wrap the whole thing into a retry block as a side effect of us building multiple branches in one workspace. Sometimes, we get a “library corrupted” failure when switching. Running a second time makes it go away – no explicit Clean required. Lots of getting Unity running on Jenkins is just experimenting and making do with what works!

You also see some examples of groovy’s string interpolation. I admit I’m no expert, bu there seems to be about a dozen ways of doing string interp in groovy, and not all approaches work in all locations. If one didn’t work, I went on to the next, and what you see here is the one that works here.

Talking to Unity

We need to convince Unity to do what we want, and we want any failures to produce useful output in the Jenkins dashboard. You can find more in the Unity docs but I found the best way to get output was to have -logFile last, with no path set.

To convince Unity to do what we want, we use the -executeMethod parameter. That will call a static c# function of your choice. How to make builds from within Unity is outside the scope of this blog post.

Here’s our next few stages, and the various ways they call to Unity:

stage ('Editmode Tests') {
    steps {
        bat "\"${UNITY_PATH}\" -nographics -batchmode -projectPath . -runTests -testResults editmodetests.xml -testPlatform editmode -logFile"
    }
}
stage ('Playmode Tests') {
    when {
        expression {
            return (params.BUILD == 'Deploy' || params.BUILD == 'HealthCheck') && !params.SKIP_PLAYMODE_TESTS
        }
    }
    steps {
        // no -nographics on playmode tests. they don't log right now? which is weird cuz the editmode tests do with almost the same setup.
        bat "\"${UNITY_PATH}\" -batchmode -projectPath . -runTests -testResults playmodetests.xml -testPlatform playmode -testCategory \"BuildServer\" -logFile"
    }
}
stage ('Build') {
    steps {
        bat "\"${UNITY_PATH}\" -nographics -buildTarget Win64 -quit -batchmode -projectPath . -executeMethod MyBuildFunction  -MyBuildType \"${params.TYPE}\" -logFile"
    }
}

Deployment

This is project and platform specific, so I won’t go into details, but let’s assume you’re zipping or packaging a build folder and sending somewhere.

Here we’d be able to use the branch environment variables to maybe choose a destination folder. We’re also able to reuse the build name environment variables. We created both of those in Prewarm.

stage ('Deploy') {
    when {
        expression { return params.BUILD == 'Deploy' }
    }
    steps {
        // ... how to get a build to your platform of choice ...
        slackSend color: 'good', message: ":ship: build ${env.BUILD_DISPLAY_NAME} DEPLOYED (<${env.BUILD_URL}|Open>)", channel: "project_builds"
    }
}

We also post that the build has been deployed. More on notifications below.

Notifications and wrap-up

The post section of the jenkinsfile contains blocks that will run after the main job, in different circumstances. We mostly use them to report progress to slack:

post {
    always {
        nunit testResultsPattern: '*tests.xml'
    }
    success {
        script {
            if (params.BUILD == 'HealthCheck') {
                slackSend color: 'good',
                    message: ":green_heart: build ${env.BUILD_DISPLAY_NAME} SUCCEEDED (<${env.BUILD_URL}|Open>)",
                    channel: "project_builds"
            }
        }
    }
    fixed {
        slackSend color: 'good',
            message: ":raised_hands: build ${env.BUILD_DISPLAY_NAME} FIXED (<${env.BUILD_URL}|Open>)",
            channel: "project_builds"
    }
    aborted {
        slackSend color: 'danger',
            message: ":warning: build ${env.BUILD_DISPLAY_NAME} ABORTED. Was it intentional? (<${env.BUILD_URL}|Open>)",
            channel: "project_builds"
    }
    failure {
        slackSend color: 'danger',
            message: ":red_circle: build ${env.BUILD_DISPLAY_NAME} FAILED (<${env.BUILD_URL}|Open>)",
            channel: "project_builds"
    }
}                    

The first step here is to always report the automated test results to Jenkins with the nunit plugin. Unity’s test reports are in the nunit format, and this plugin converts it to the junit format that Jenkins expects.

We post all failures to the slack channel, and all fixed builds, but we don’t post all successes. With builds on every push that might make the build channel too noisy. We do however post when Health Checks succeed, since that’s good affirmation.

We use the Slack Notification plugin. The slack color attributes here doesn’t seem to work for us? So we use emojis to make it easy to scan what’s happening. Here’s an example from slack:

Porting an existing job to pipelines

Jenkins includes a snippet generator, which allows you to make freestyle blocks and see the generated pipeline script, which is very handy for porting freestyle jobs:

The full file

UNITY_PATH = "C:\\Program Files\\Unity\\Hub\\Editor\\2019.4.5f1\\Editor\\Unity.exe"
pipeline {
    parameters {
        choice(name: 'TYPE', choices: ['Debug', 'Release', 'Publisher'], description: 'Do you want cheats or speed?')
        choice(name: 'BUILD', choices: ['Fast', 'Deploy', 'HealthCheck'], description: 'Fast builds run minimal tests and make a build. HealthCheck builds run more automated tests and are slower. Deploy builds are HealthChecks + a deploy.')
        booleanParam(name: 'CLEAN', defaultValue: false, description: 'Tick to removed cached files - will take an eon')
        booleanParam(name: 'SKIP_PLAYMODE_TESTS', defaultValue: false, description: 'In an emergency, to allow Deploy builds to work with a failing playmode test')
    }
    agent {
        node {
            label "My Project"
            // force everyone to the space space, so we can share a library file.
            customWorkspace 'workspace\\MyProject'
        }
    }
    options {
        timestamps()
        // as a failsafe. our build tend around the 15min mark, so 45 would be excessive.
        timeout(time: 45, unit: 'MINUTES')
    }
    // post stages only kick in once we definitely have a node
    stages {
        stage('Clean') {
            when {
                expression { return params.CLEAN }
            }
            steps {
                bat "if exist Library (rmdir Library /s /q)"
                bat "if exist Temp (rmdir Temp /s /q)"
            }
        }
        stage('Prewarm') {
            steps {
                script {
                    // not easy to get jenkins to tell us the current branch! none of the built-in envs seem to work?
                    // let's just ask svn directly.
                    def get_branch_script = 'svn info | select-string "Relative URL: \\^\\/(.*)" | %{\$_.Matches.Groups[1].Value}'
                    env.R7_SVN_BRANCH = powershell(returnStdout: true, script:get_branch_script).trim()
                }
                buildName "${BUILD_NUMBER} - ${TYPE}@${env.R7_SVN_BRANCH}/${SVN_REVISION}"
                script {
                    if (params.BUILD == 'Deploy') {
                        buildName "${env.BUILD_DISPLAY_NAME} ^Deploy"
                    }
                    if (params.BUILD == 'HealthCheck') {
                        buildName "Health Check of ${TYPE}@${env.R7_SVN_BRANCH}/${SVN_REVISION}"
                    }
                    if (params.BUILD == 'HealthCheck' || params.BUILD == 'Deploy') {
                        // let's warn that a deploy build is in progress
                        slackSend color: 'good',
                            message: ":hourglass: build ${env.BUILD_DISPLAY_NAME} STARTED (<${env.BUILD_URL}|Open>)",
                            channel: "project_builds"
                    }
                }
                // clean tests
                bat "if exist *tests.xml (del *tests.xml)"
                // need an initial open/close to clean out the serialisation. without this you can get things validating on old code!!
                // do it twice, to make it more tolerant of bad libraries when switching branches
                retry(count: 2) {
                    bat "\"${UNITY_PATH}\" -nographics -buildTarget Win64 -quit -batchmode -projectPath . -executeMethod MyBuildFunction -MyBuildType \"${params.TYPE}\" -MyTestOnly -logFile"
                }
            }
        }
        stage ('Editmode Tests') {
            steps {
                bat "\"${UNITY_PATH}\" -nographics -batchmode -projectPath . -runTests -testResults editmodetests.xml -testPlatform editmode -logFile"
            }
        }
        stage ('Playmode Tests') {
            when {
                expression {
                    return (params.BUILD == 'Deploy' || params.BUILD == 'HealthCheck') && !params.SKIP_PLAYMODE_TESTS
                }
            }
            steps {
                // no -nographics on playmode tests. they don't log right now? which is weird cuz the editmode tests do with almost the same setup.
                bat "\"${UNITY_PATH}\" -batchmode -projectPath . -runTests -testResults playmodetests.xml -testPlatform playmode -testCategory \"BuildServer\" -logFile"
            }
        }
        stage ('Build') {
            steps {
                bat "\"${UNITY_PATH}\" -nographics -buildTarget Win64 -quit -batchmode -projectPath . -executeMethod MyBuildFunction  -MyBuildType \"${params.TYPE}\" -logFile"
            }
        }
        stage ('Deploy') {
            when {
                expression { return params.BUILD == 'Deploy' }
            }
            steps {
                // ... how to get a build to your platform of choice ...
                slackSend color: 'good', message: ":ship: build ${env.BUILD_DISPLAY_NAME} DEPLOYED (<${env.BUILD_URL}|Open>)", channel: "project_builds"
            }
        }        
    }
    post {
        always {
            nunit testResultsPattern: '*tests.xml'
        }
        success {
            script {
                if (params.BUILD == 'HealthCheck') {
                    slackSend color: 'good',
                        message: ":green_heart: build ${env.BUILD_DISPLAY_NAME} SUCCEEDED (<${env.BUILD_URL}|Open>)",
                        channel: "project_builds"
                }
            }
        }
        fixed {
            slackSend color: 'good',
                message: ":raised_hands: build ${env.BUILD_DISPLAY_NAME} FIXED (<${env.BUILD_URL}|Open>)",
                channel: "project_builds"
        }
        aborted {
            slackSend color: 'danger',
                message: ":warning: build ${env.BUILD_DISPLAY_NAME} ABORTED. Was it intentional? (<${env.BUILD_URL}|Open>)",
                channel: "project_builds"
        }
        failure {
            slackSend color: 'danger',
                message: ":red_circle: build ${env.BUILD_DISPLAY_NAME} FAILED (<${env.BUILD_URL}|Open>)",
                channel: "project_builds"
        }
    }                    
}

Congrats to reading to the end! Your prize is a nice fish: 🐟

Running unity tests on a build server

(Read part 1, about creating parametric level smoke tests, here)

Now we have some useful tests, we want our Jenkins install to run them frequently. That way, we really quick feedback on failures. Here’s how we do this at Roll7.

Running tests from the command line

Let’s assume you already have a server to build your game. I won’t cover how to set up a unity Jenkins build server here, except to say that the Unity3d plugin is very helpful when you have lots of unity versions. (Although we couldn’t get the log parameter to be picked up unless we specified the path fully in our command line arguments!)

The command line to run a Unity Test Framework job is as follows:

-batchmode -projectPath "." -runTests -testResults playmodetests.xml -testPlatform playmode -logFile ${WORKSPACE}\playtestlog.txt

You can read more about command line params here, but let’s break that down:

  • -batchmode tells unity not to open a GUI, which is vital for a build server. We don’t want it to hang on a dialog! You can also use Application.isBatchMode to test for this flag in your code.
  • -projectPath "." just tells unity to load the project in our working directory
  • -runTests starts a Unity Test Framework job as soon as the editor loads. It’ll run some specified tests, spit out some output, and make sure test failures cause a non-zero return code.
  • -testPlatform playmode tells the UTF to run our playmode tests, which are the ones we care about for this blog post. You can also use editmode.
  • -testResults playmodetests.xml states where to spit out a report of the run, which will include failures as well as logs. The report is formatted as an nunit test result XML. Jenkins has a plugin that can fail a job based on this file, and present a nice reporting UI.
  • -logFile ${WORKSPACE}\playtest.txt specifies where to write the editor log – by default it won’t stream into the console. The ${WORKSPACE} is a jenkins environment variable, and we found specifying it was the only way to get the unity3d plugin to find the log.

…And that’s enough to do a test run.

Playmode tests in batch

This page of the docs mentions that the WaitForEndOfFrame coroutine is a bad idea in batch mode. This is because “systems like animation, physics and timeline might not work correctly in the Editor”.

There’s not much more detail than this!

In practice, we’ve found any playmode tests that depend on movement, positioning or animation fails pretty reliably. We get around this by explicitly marking tests we know can run on the server with the [Category("BuildServer")] attribute. We can then use the -testCategory "BuildServer" parameter to only run these tests.

This is pretty limiting! But there’s still plenty of value in just making sure your levels, enemies and weapons work without firing errors or warnings.

In the near future we’ll be experimenting with an ec2 instance that has a GPU, to let us run without batchmode, and also allow us to run playmode tests on finished builds more easily.

Gotchas and practicalities

  • Currently, we find a full playmode test run is exceedingly slow, and we don’t yet know why. What takes a minute or two locally takes tens of minutes on the ec2 instance. It’s not a small instance, either! So we’re only scheduling a test run for our twice-daily steam builds, instead of for every push.
  • WaitForEndOfFrame doesn’t work in batch mode, so beware of using that in your tests, or anything your tests depend on.
  • The vagaries of unity’s compile step mean that some sometimes you can get out-of-date OnValidate calls running on new data as you open the editor. Maybe this is fixable with properly defined assemblies, but we hackily get around it by doing a dummy build right at the start of the jenkins job. It goes as far as setting the correct platform, waits for all the compiles, and quits. Compile errors still cause failures, which is good.
  • If you want editmode and playmode tests in the same job, just run the editor twice. We do this with two different testResults xmls, and we can use a wildcard in the nunit jenkins plugin to pick up both.
  • To test your command line in dos, use the start command: start /wait "" "path to unity.exe" -batchmode ... The extra empty spaces are important if your unity path has spaces in it too. To see the last command’s return code in dos, use echo %ERRORLEVEL%.
  • These are running playmode tests in the editor on the build server. We haven’t yet got around to making playmode tests work in a build. That might end up as a follow-up post!

Smoke-testing scenes, using Jenkins and Unity Test Framework

You may be familiar with Unity’s Test Runner window, where you can execute tests and see results. This is the user-facing part of the Unity Test Framework, which is a very extensible system for running tests of any kind. At Roll7 I recently set up the test runner to automatically run simple smoketests on every level of our (unannounced) game, and made jenkins report on failures. In this post I’ll outline how I did the former, and in part two I’ll cover the later.

Play mode tests, automatically generated for every level
Some of our [redacted] playmode and editmode tests, running on Jenkins

(I’m going to assume you have passing knowledge of how to write tests for the test runner)

(a lot of this post is based on this interesting talk about UTF from its creators at Unite 2019)

The UTF is built upon NUnit, a .net testing framework. That’s what provides all those [TestFixture] and [Test] attributes. One feature of NUnit that UTF also supports is [TestFixtureSource]. This attribute allows you to make a sort of “meta testfixture”, a template for how to make test fixtures for specific resources. If you’re familiar with parameterized tests, it’s like that but on a fixture level.

We’re going to make a TestFixtureSource provider that finds all level scenes in our project, and then the TestFixutreSource itself that loads a specific level and runs some generic smoke tests on it. The end result is that adding a new level will automatically add an entry for it to the play mode tests list.

There’s a few options for different source providers (see the NUnit docs for more), but we’re going to make an IEnumerable that finds all our level scenes. The results of this IEnumerable are what gets passed to our constructor – you could use any type here.

class AllRequiredLevelsProvider : IEnumerable<string>
{
    IEnumerator<string> IEnumerable<string>.GetEnumerator()
    {
        var allLevelGUIDs = AssetDatabase.FindAssets("t:Scene", new[] {"Assets/Scenes/Levels"} );
        foreach(var levelGUID in allLevelGUIDs)
        {
            var levelPath = AssetDatabase.GUIDToAssetPath(levelGUID);
            yield return levelPath;
        }
    }
    public IEnumerator GetEnumerator() => (this as IEnumerable<string>).GetEnumerator();
}

Our TestFixture looks like a regular fixture, except also with the source attribute linking to our provider. Its constructor takes a string that defines which level to load.

[TestFixtureSource(typeof(AllRequiredLevelsProvider))]
public class LevelSmokeTests
{
    private string m_levelToSmoke;
    public LevelSmokeTests(string levelToSmoke)
    {
        m_levelToSmoke = levelToSmoke;
    }

Now our fixture knows which level to test, but not how to load it. TestFixtures have a [SetUp] attribute which runs before each test, but loading the level fresh for each test would be slow and wasteful. Instead let’s use [OneTimeSetup] (πŸ‘€ at the inconsistent capitalisation) and to load and unload our level for each fixture. This depends somewhat on your game implementation, but for now let’s go with UnityEngine.SceneManagement:

// class LevelSmokeTests {
    [OneTimeSetUp]
    public void LoadScene()
    {
        SceneManager.LoadScene(m_levelToSmoke);
    }

Finally, we need some tests that would work on any level we throw at it. The simplest approach is probably to just watch the console for errors as we load in, sit in the level, and then as we load out. Any console errors at any of these stages should fail the test.

UTF provides LogAsset to validate the output of the log, but at this time it only lets you prescribe what should appear. We don’t care about Debug.Log() output, but want to know if there was anything worse than that. Particularly, in our case, we’d like to fail for warnings as well as errors. Too many “benign” warnings can hide serious issues! So, here’s a little utility class called LogSeverityTracker, that helps check for clean consoles. Check the comments for usage.

Our tests can use the [Order] attribute to ensure they happen in sequence:

// class LevelSmokeTests {
    [Test, Order(1)]
    public void LoadsCleanly()
    {
        m_logTracker.AssertCleanLog();
    }

    [UnityTest, Order(2)]
    public IEnumerator RunsCleanly()
    {
        // wait some arbitrary time
        yield return new WaitForSeconds(5);
        m_logTracker.AssertCleanLog();
    }

    [UnityTest, Order(3)]
    public IEnumerator UnloadsCleanly()
    {
        // how you unload is game-dependent 
        yield return SceneManager.LoadSceneAsync("mainmenu");
        m_logTracker.AssertCleanLog();
    }

Now we’re at the point where you can hit Run All in the Test Runner and see each of your levels load in turn, wait a while, then unload. You’ll get failed tests for console warnings or errors, and newly-added levels will get automatically-generated test fixtures.

More tests are undoubtedly more useful than less. Depending on the complexity and setup of your game, the next steps might be to get the player to dumbly walk around for a little bit. You can get a surprising amount of info from a dumb walk!

In part 2, I’ll outline how I added all this to jenkins. It’s not egregiously hard, but it can be a bit cryptic at times.

Adding a custom movement mode to Unreal’s CharacterMovementComponent via blueprints

This isn’t a full tutorial, and I’m not an expert, but I noticed this knowledge wasn’t really collected together anywhere, so I’m putting something together here. Please shout if there’s any holes or mistakes.

The CharacterMovementComponentΒ that comes with the third-person starter kit has several movement modes you can switch between using the Set Movement Mode node. Walking, falling, swimming, and flying are all supported out-of-the-box, but there’s also a “Custom” option in the dropdown. How do you implement a new custom movement mode?

First, limitations: I’ve not found a way to make networkable custom movement modes via blueprint. I think I need to be reusing the input from Add Movement Input, but I’m not yet sure how. Without doing that, the server has no idea how to do your movement.

When you set a new movement mode, the OnMovementModeChanged event (which is undocumented??) gets fired:

movementmodechanged

At this point you can toggle state or meshes, zero velocity, and other things you might want to do when entering and leaving your custom mode.

The (also undocumented) UpdateCustomMovement event will fire when you need to do movement:

updatemovementmode.PNG

From here you can read your input axis and implement your behaviours. You can just use the delta and Set Actor Location, but there’s also the Calc Velocity node which can help implement friction and deceleration for you.

To return to normal movement again, I’ve found it’s safest to use Set Movement Mode to enter Falling, and let the component sort itself out, but ymmv there.

Hope someone finds this helpful.