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.
How a GameDriver test runs in a pipeline
Section titled “How a GameDriver test runs in a pipeline”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:
| Mode | Where the game runs | How the test connects |
|---|---|---|
| Standalone | On the runner itself | The test launches the build, then connects to localhost |
| Remote target | On another machine or device: a test PC, a phone, a console dev kit or a headset | The 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.
Step 1: Parameterize the connection
Section titled “Step 1: Parameterize the connection”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(); }}Unreal Engine
Section titled “Unreal Engine”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.
Startup time
Section titled “Startup time”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:
| Variable | Example | Used in |
|---|---|---|
GD_MODE | standalone or remote | Both modes |
GD_HOST | 192.168.10.42 or quest-lab-01 | Remote target |
GD_PORT | 19734 (Unity) or 15505 (Unreal) | Both modes |
GD_EXECUTABLEPATH | C:\builds\MyGame\MyGame.exe | Standalone |
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 Linuxnc -zv 192.168.10.42 19734Step 4: Add the pipeline
Section titled “Step 4: Add the pipeline”GitHub Actions
Section titled “GitHub Actions”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.
GitLab CI
Section titled “GitLab CI”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.xmlGitLab shows JUnit reports on the merge request, so failed GameDriver tests appear next to the change that caused them.
Standalone mode
Section titled “Standalone mode”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"Other CI systems
Section titled “Other CI systems”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 projects
Section titled “XR projects”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.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Check |
|---|---|
| The test cannot connect | Run 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 disconnects | The GameDriver API DLLs in the test project must match the plugin version in the build. Update both together. |
| Standalone mode times out | Give 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 CI | Compare the GD_ variables and the build the runner received with your local setup. |
More connection errors are covered in Connectivity Issues.