Skip to content

Running GameDriver Tests in CI/CD

GameDriver tests are NUnit tests. Anything that can build and run an NUnit test project can run them, which means the same setup carries across GitHub Actions, GitLab CI, Jenkins, Azure DevOps, TeamCity and others. This guide covers the pieces that stay the same on every system, then shows example pipelines for GitHub Actions and GitLab CI.

If you have not run your tests from a command line yet, start with Running GameDriver Tests from the Command Line.

The test runs on the CI runner. It connects to the GameDriver agent inside your game, and there are two ways to set that up:

ModeWhere the game runsHow the test connects
StandaloneOn the runner itselfThe test launches the build, then connects to localhost
Remote targetOn another machine or device: a test PC, a phone, a console dev kit or a headsetThe test connects to the target’s IP address or hostname

Either way, the test needs a network path to the agent’s port. The defaults are 19734 for Unity and 15505 for Unreal Engine.

Hard-coded hosts and paths tie a test to one machine. Read them from parameters instead, so the same test project runs on a developer’s desktop and in every pipeline.

The examples below check NUnit test parameters first, then environment variables, then fall back to a default. Environment variables are the most portable option, since every CI system can set them. Unity and Unreal Engine follow the same pattern. Only the namespace, the default port and the editor connection differ.

using System;
using NUnit.Framework;
using gdio.unity_api;
using gdio.unity_api.v2;
using gdio.common.objects;
[TestFixture]
public class SmokeTests
{
ApiClient api;
// NUnit parameter first, then environment variable (GD_MODE, GD_HOST...), then default
static string Param(string name, string fallback) =>
TestContext.Parameters.Get(name, null)
?? Environment.GetEnvironmentVariable("GD_" + name.ToUpperInvariant())
?? fallback;
string mode = Param("Mode", "IDE"); // IDE, standalone or remote
string host = Param("Host", "localhost");
int port = int.Parse(Param("Port", "19734"));
string exePath = Param("ExecutablePath", null);
[OneTimeSetUp]
public void Connect()
{
api = new ApiClient();
if (mode == "standalone" && exePath != null)
{
// Launch the build on this runner, then connect locally
ApiClient.Launch(exePath);
api.Connect("localhost", port, false, 60);
}
else if (mode == "remote")
{
// Connect to a build already running on a target on the local network
api.Connect(host, port, false, 60);
}
else
{
// Local development: connect to the Unity Editor and enter Play mode
api.Connect("localhost", port, true, 30);
}
api.EnableHooks(HookingObject.ALL);
}
[OneTimeTearDown]
public void Disconnect()
{
api.DisableHooks(HookingObject.ALL);
api.Disconnect();
}
}
using System;
using NUnit.Framework;
using gdio.unreal_api;
using gdio.common.objects;
[TestFixture]
public class SmokeTests
{
ApiClient api;
static string Param(string name, string fallback) =>
TestContext.Parameters.Get(name, null)
?? Environment.GetEnvironmentVariable("GD_" + name.ToUpperInvariant())
?? fallback;
string mode = Param("Mode", "IDE"); // IDE, standalone or remote
string host = Param("Host", "localhost");
int port = int.Parse(Param("Port", "15505"));
string exePath = Param("ExecutablePath", null);
[OneTimeSetUp]
public void Connect()
{
api = new ApiClient();
if (mode == "standalone" && exePath != null)
{
// Launch the build on this runner, wait for it to start, then connect locally
ApiClient.Launch(exePath);
api.Wait(10000);
api.Connect("localhost", port);
}
else if (mode == "remote")
{
// Connect to a build already running on a target on the local network
api.Connect(host, port);
}
else
{
// Local development: connect to the editor, already in Play mode
api.Connect("localhost", port);
}
}
[OneTimeTearDown]
public void Disconnect()
{
api.Disconnect();
}
}

If you changed the agent port in gdio.unreal_agent.config.txt, set GD_PORT to match.

In the Unity example, the last argument to Connect is the timeout in seconds. In the Unreal example, the wait after Launch does the same job. Give standalone launches and slower targets more time than the editor, since the build has to start before the agent can answer.

Step 2: Run the tests from the command line

Section titled “Step 2: Run the tests from the command line”

Use the same command locally that the pipeline will use. With the .NET SDK:

dotnet test Tests/MyGame.Tests.csproj -c Release --logger "junit;LogFilePath=results/junit.xml"

The junit logger comes from the JunitXml.TestLogger NuGet package. Add it to your test project so every CI system can read the results. If you use the NUnit Console Runner instead, it writes TestResult.xml in NUnit 3 format, and parameters go in with --testparam:Mode=remote.

Set the parameters through environment variables:

VariableExampleUsed in
GD_MODEstandalone or remoteBoth modes
GD_HOST192.168.10.42 or quest-lab-01Remote target
GD_PORT19734 (Unity) or 15505 (Unreal)Both modes
GD_EXECUTABLEPATHC:\builds\MyGame\MyGame.exeStandalone

Step 3: Set up a runner on the local network

Section titled “Step 3: Set up a runner on the local network”

Install your CI system’s self-hosted runner on a machine that sits on the same network as your targets, then give it a label so GameDriver jobs only land there:

  • GitHub Actions: a self-hosted runner with a custom label such as gamedriver
  • GitLab CI: a GitLab Runner with the shell executor and a tag such as gamedriver
  • Jenkins: an agent node with a label such as gamedriver

The runner needs the .NET SDK your test project targets. For standalone mode it also needs the hardware and drivers to run your game, and it should run under a logged-in user session rather than as a background service, so the game has a desktop to render to.

Before the first pipeline run, confirm the runner can reach the target from its own command line:

# Windows (PowerShell)
Test-NetConnection 192.168.10.42 -Port 19734
# macOS or Linux
nc -zv 192.168.10.42 19734
name: GameDriver tests
on:
push:
branches: [main]
workflow_dispatch:
jobs:
gamedriver:
runs-on: [self-hosted, gamedriver]
env:
GD_MODE: remote
GD_HOST: ${{ vars.GD_TARGET_HOST }}
GD_PORT: "19734" # 15505 for Unreal Engine
steps:
- uses: actions/checkout@v4
- name: Run GameDriver tests
run: dotnet test Tests/MyGame.Tests.csproj -c Release --logger "junit;LogFilePath=results/junit.xml"
- name: Keep the results
if: always()
uses: actions/upload-artifact@v4
with:
name: gamedriver-results
path: Tests/results/

Store the target address as a repository variable (GD_TARGET_HOST) so a new device does not need a code change.

gamedriver-tests:
stage: test
tags: [gamedriver]
variables:
GD_MODE: "remote"
GD_HOST: "$GD_TARGET_HOST"
GD_PORT: "19734" # 15505 for Unreal Engine
script:
- dotnet test Tests/MyGame.Tests.csproj -c Release --logger "junit;LogFilePath=results/junit.xml"
artifacts:
when: always
reports:
junit: Tests/results/junit.xml

GitLab shows JUnit reports on the merge request, so failed GameDriver tests appear next to the change that caused them.

To run against a build on the runner itself, fetch the build in an earlier step (from your build job’s artifacts or a shared location) and change the variables:

GD_MODE: "standalone"
GD_EXECUTABLEPATH: "C:\\builds\\MyGame\\MyGame.exe"

Jenkins, Azure DevOps, TeamCity and others follow the same pattern: route the job to a runner on the local network, set the GD_ variables, run dotnet test, and publish the JUnit file.

XR tests can run in a pipeline without a headset attached. GameDriver creates simulated headsets and controllers from the test, so the runner only needs the build. See Working with the Unity Input System, Working with Legacy XR and OVR in Unity and Simulating Unreal XR inputs.

SymptomCheck
The test cannot connectRun the reachability check from Step 3 on the runner. Confirm the firewall on the target allows the agent port, and that the agent is enabled in the build (in Unity, in the first scene loaded).
The test connects, then disconnectsThe GameDriver API DLLs in the test project must match the plugin version in the build. Update both together.
Standalone mode times outGive the build more time to start (the Unity Connect timeout or the Unreal wait after Launch), and confirm the runner is in a logged-in desktop session.
Tests pass locally and fail in CICompare the GD_ variables and the build the runner received with your local setup.

More connection errors are covered in Connectivity Issues.