How to run WPF and WinForms tests in CI on a Windows runner
A WPF or WinForms test may pass on a workstation yet fail to find the application’s window after it moves to continuous integration (CI), the automated process that builds and checks changes. The test is not necessarily at fault: a desktop interface needs a suitable Windows session, whereas a CI agent often runs as a service without a usable display. This article explains how to select a runner, prepare its session, and distinguish an application defect from an environment failure.
Why a desktop UI test is not an ordinary CI job
A unit or API test can run without a signed-in user. A graphical test launches the application, finds windows through Microsoft UI Automation, and—depending on the library—uses focus, keyboard input, a mouse, or screenshots. It therefore needs a session in which the desktop is available in the way required by the particular tool.
A local run meets this condition without special configuration: the user is signed in and can see the application. In CI, the runner may use another account, run as a Windows service, operate in a locked session, or lack dependencies installed on the author’s machine. Verify the environment before changing the test’s locators and waits.
Hosted and self-hosted runners serve different needs
A hosted Windows runner is useful for building the application, unit tests, and other tasks that do not require a permanently prepared interactive desktop. For example, GitHub Actions provisions a new hosted virtual machine for every job and decommissions it afterwards. Software can be installed during the job, but a windows-latest label alone does not show that a particular desktop tool will receive the session it needs.
Conditions also vary between CI platforms. Microsoft states that visible UI testing is not supported on Microsoft-hosted Azure Pipelines agents. For desktop tests on a self-hosted Windows agent, it describes running the agent interactively with automatic sign-in. That statement applies to the named platform; with GitHub Actions, GitLab, or another solution, use a pilot to verify how both the runner and test library are launched.
A self-hosted runner gives the team control over Windows, installed runtimes, the account, and the machine’s lifecycle. The team also becomes responsible for updates, access protection, data cleanup, and availability. A controlled virtual machine is often a more practical foundation for visible WPF or WinForms testing, but it does not guarantee stability.
Example: the test fails only on the overnight run
A team has an order-creation test that passes on its author’s workstation. A technician also runs it successfully on the CI virtual machine through Remote Desktop (RDP) in the evening. The overnight job then reports that the Save button does not exist. Its screenshot shows the Windows sign-in screen rather than the application.
This failure does not yet prove that the locator is wrong. The decisive questions are which account ran the runner, which session launched the application, what happened when the technician disconnected, and whether the application process was visible in the test’s session. Adding a longer wait or automatic retry without checking these conditions may merely hide a configuration that fails on every unattended run.
The pilot must therefore use the real launch method: a scheduled trigger, disconnected remote administration, the intended test account, and the same artefact as the future overnight job. Investigating individual application controls becomes worthwhile only after this run passes.
How to prepare the Windows runner
1. Prove a pilot on the target machine
Run one representative scenario manually on the future runner first. It should launch the application, work with both a common form and a more complex control, and verify the result. If you have not yet established that the tool can see the controls, start with the technical assessment in How to automate WPF and WinForms application testing.
2. Use a dedicated, reproducible environment
Record the Windows version, .NET runtime, drivers, fonts, language, regional settings, and every service on which the application depends. Install the same artefact produced by the pipeline, not an arbitrary developer build. A personal workstation is a poor foundation because updates, pop-ups, and manual activity change its state while tests run.
3. Prepare the account and interactive session
The runner and application must execute in a session accessible to the test process. If the platform and library require automatic sign-in, use a separate test account with the least privileges needed and protect access to the virtual machine. Do not store its password in the repository or a readable script.
If the application needs elevated privileges, verify that the privilege levels of the test process and application are compatible. Do not globally disable User Account Control (UAC) or other protections merely to make a test pass. Adjust the pilot, account, or deployment method instead, and document the permissions that are genuinely necessary.
4. Prevent unintended session locking or changes
Configure the environment so scheduled sleep policies, a screen saver, or interactive updates do not interrupt tests during the agreed window. Take care with remote administration: Microsoft warns that simply disconnecting RDP from an agent configured for automatic sign-in can lock the machine and cause UI tests to fail. The Azure guidance describes disconnecting with tscon; use this only after verifying the procedure for your platform and session. A hypervisor console or remote-access method that does not lock the test desktop is another option.
5. Restore the application and data before each run
Before testing, stop processes left by the previous run, deploy the artefact, and prepare known data. Afterwards, remove created files and restore the database or user profile according to the agreed procedure. A clean state is more valuable than trying to mask every failure with a retry.
6. Parallelise across machines, not one desktop
Two tests on one interactive desktop can steal focus, close each other’s windows, or share the same clipboard. Begin with one UI worker. If the suite needs to run faster, divide it across isolated virtual machines or separate sessions whose support you have verified with the chosen tool.
7. Preserve evidence about the environment and failure
For each run, record the application, runner, and Windows versions, display resolution, and scaling. On failure, preserve the test and application logs, a screenshot, and, where possible, the UI Automation tree. Artefacts may expose customer data or passwords, so restrict their access and retention.
Acceptance criteria for the runner
Do not judge runner readiness from one green run. Create a short infrastructure smoke test that checks the session, desktop availability, application startup, artefact version, and access to test data before product scenarios begin. If this check fails, the pipeline should classify the result as an environment failure and avoid running product tests.
Repeat the pilot under conditions the runner will encounter in operation:
- after the virtual machine restarts and the agent launches automatically;
- after a clean application installation and after an update;
- without an open RDP window or manual user intervention;
- with the same resolution, scaling, language, and regional settings;
- after a failed test, to confirm that processes stop and data is restored.
Completing the UI steps alone is not sufficient. Verify that the test used the intended build, the result was created in the designated environment, and the next run began from a clean state. If the application creates a local file or database record, check its contents through a stable interface rather than relying only on a confirmation-dialog message.
Divide the pipeline by purpose
A practical pipeline builds and signs the application on a regular build runner and then sends that exact artefact to a dedicated Windows UI runner. A short smoke pack verifies startup, sign-in, and one critical process. The broader regression suite can run after an important merge, before release, or overnight according to its duration and risk.
Classify environment-preparation failures separately from incorrect application behaviour. If the session could not sign in or the artefact could not be installed, the result does not say that the product failed its test. This distinction saves diagnostic time and protects trust in the suite.
Use automatic retries cautiously. One diagnostic retry can show whether a failure depends on a transient state, but the original result must remain visible. Track the proportion of infrastructure smoke-test failures separately from product tests that pass only after a retry. An increase in the first points towards the runner; an increase in the second may involve the test, application, or shared data and needs separate investigation.
What you gain
A dedicated runner and documented session turn a local script into a repeatable check of a specific build. The team can tell whether a failure belongs to the application, test, or infrastructure and has enough evidence to reproduce it. Builds and fast tests can remain on hosted runners while the more sensitive desktop layer operates within a controlled scope.
Next step
Choose one critical WPF or WinForms scenario and run it ten times on the intended runner without remote intervention. Record the session, account, versions, data, and diagnostics. Add a release gate and expand the suite only after the pilot can reliably distinguish an environment failure from a product defect.