Multi-Project Test Setup with Symbolic Links
December 29, 2025 · View on GitHub
An advanced example demonstrating how to test Godot projects in a multi-project solution using symbolic links to preserve resource paths and maintain clean separation between production and test code.
What This Example Shows
This project demonstrates:
- Multi-project test architecture with separate test and production projects
- Symbolic link strategy for resource path preservation
- Single assembly compilation without ProjectReference
- Cross-platform compatibility for Windows, Linux, and macOS
- Git-friendly setup with proper .gitignore configuration
- CI/CD ready structure with automated symlink creation
Why Use Multi-Project Setup?
Advantages over Single Project
- Clean Separation: Tests don't bloat production builds
- Independent Dependencies: Test-only packages stay in test project
- Better Organization: Clear boundaries between test and production code
- Flexible Deployment: Ship production without test dependencies
- IDE Performance: Faster IntelliSense with smaller project scopes
When to Use
- ✅ Large projects requiring extensive test coverage
- ✅ Team projects with dedicated QA/test developers
- ✅ Production builds where size and dependencies matter
- ✅ Projects following enterprise architecture patterns
- ✅ When you need different build configurations for test vs production
Files Overview
Main Project (ExampleProject/)
ExampleProject.csproj- Production project with Godot SDKproject.godot- Godot project configurationsrc/- Source code, scenes, and resources
Test Project (ExampleProject.Test/)
ExampleProject.Test.csproj- Test project with GdUnit4 configuration and InitialTargets.gitignore- Excludes symlinked folders from version controltest/- Test files and test-specific resourcessrc/- Symlink to main project's src folder (created automatically before build)
Key Configuration: InitialTargets
The critical part of this setup is using InitialTargets to create the symlink before MSBuild evaluates which files to compile:
<Project Sdk="Godot.NET.Sdk/4.5.1" InitialTargets="CreateSymlinks">
This ensures:
- ✅ Symlink exists before MSBuild scans for .cs files
- ✅ No explicit
<Compile Include>directives needed - ✅ Works consistently in both local and CI environments
Key Configuration Sections
1. Test Project Configuration
<Project Sdk="Godot.NET.Sdk/4.5.1" InitialTargets="CreateSymlinks">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>GdUnit4.Examples.Advanced.Setup.MultiProjectSetup</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<!-- Ensures all dependencies are copied to output for test execution -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Identifies this as a GdUnit4 test project for test discovery -->
<TestFramework>GdUnit4</TestFramework>
</PropertyGroup>
<ItemGroup>
<!-- NO ProjectReference - we compile from symlinked source instead -->
<!-- Core .NET testing infrastructure -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<!-- Test frameworks -->
<PackageReference Include="gdUnit4.api" Version="5.1.0-rc3"/>
<!-- Test adapter to integrate GdUnit4 with .NET test runners -->
<PackageReference Include="gdUnit4.test.adapter" Version="3.0.0"/>
</ItemGroup>
</Project>
2. Symbolic Link Creation (InitialTargets)
<!-- This target runs BEFORE MSBuild evaluates files, thanks to InitialTargets -->
<Target Name="CreateSymlinks">
<PropertyGroup>
<SrcSource>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../ExampleProject/src'))</SrcSource>
<SrcTarget>$(MSBuildProjectDirectory)/src</SrcTarget>
</PropertyGroup>
<!-- Windows: Creates directory symlink or junction as fallback -->
<Exec Command="if not exist "$(SrcTarget)" mklink /D "$(SrcTarget)" "$(SrcSource)""
Condition="'$(OS)' == 'Windows_NT'"
ContinueOnError="true"/>
<!-- Unix/Linux/Mac: Creates symbolic link -->
<Exec Command="[ ! -L '$(SrcTarget)' ] && ln -s '$(SrcSource)' '$(SrcTarget)' || true"
Condition="'$(OS)' != 'Windows_NT'"
ContinueOnError="true"/>
</Target>
3. Git Configuration
# .gitignore in test project
.godot/
src # Symlinked folder - don't commit
Project Structure
Solution/
├── ExampleProject/
│ ├── ExampleProject.csproj
│ ├── project.godot
│ └── src/
│ ├── ExampleScene.tscn
│ ├── ExampleScene.cs
│ ├── Calculator.cs
│ └── scenes/
│ └── LabelScene.tscn
│
├── ExampleProject.Test/
│ ├── ExampleProject.Test.csproj [with InitialTargets="CreateSymlinks"]
│ ├── .gitignore
│ ├── src -> ../ExampleProject/src [SYMLINK - created before build]
│ └── test/
│ ├── CalculatorTest.cs
│ └── ExampleSceneTest.cs
│
└── ExampleProject.sln
How It Works
- Build starts →
InitialTargets="CreateSymlinks"runs first - Symlink created →
src/folder now points to main project - MSBuild evaluates → Finds all .cs files in
src/andtest/ - Compilation → All source files compiled into test assembly
- Tests run → Can access both code and resources
Setting Up the Project
Prerequisites
- .NET 9.0 SDK or later
- Godot 4.5+ (compatible with your Godot.NET.Sdk version)
- GdUnit4 test adapter
- Windows: Administrator privileges or Developer Mode enabled (for symlinks)
- Linux/macOS: Standard user permissions
Initial Setup
-
Clone the repository
git clone <repository> cd MultiProjectSetup -
Build the solution
dotnet buildThe symlink will be created automatically before the build starts.
-
Run tests
cd ExampleProject.Test dotnet test
Windows-Specific Setup
If symlink creation fails on Windows:
Option 1: Enable Developer Mode (Recommended)
- Open Settings → Update & Security → For Developers
- Enable "Developer Mode"
- Rebuild the project
Option 2: Run as Administrator
- Open terminal as Administrator
- Navigate to project directory
- Run
dotnet build
Option 3: Junction (Automatic Fallback)
The project automatically falls back to junctions if symlinks fail:
mklink /J src ..\ExampleProject\src
Resource Path Resolution
The symlink strategy ensures resource paths work identically in both projects:
In Main Project:
var scene = GD.Load<PackedScene>("res://src/scenes/LabelScene.tscn");
var icon = GD.Load<Texture2D>("res://src/assets/icon.png");
In Test Project (with symlink):
// Same paths work because of the symlink!
var scene = GD.Load<PackedScene>("res://src/scenes/LabelScene.tscn");
var icon = GD.Load<Texture2D>("res://src/assets/icon.png");
Why Not Use ProjectReference?
Using <ProjectReference> would cause:
- CS0436 warnings - Types compiled twice (from reference AND symlink)
- Assembly conflicts - Two versions of the same types
- Complexity - Need to exclude files from compilation
Our approach compiles everything once in the test project.
Running Tests
Command Line
# From solution root
dotnet test
# With specific verbosity
dotnet test -v detailed
# With filter
dotnet test --filter "FullyQualifiedName~CalculatorTest"
Visual Studio / Rider
- Open the solution file
- Build the solution (symlinks created automatically via InitialTargets)
- Use Test Explorer to run tests
VS Code
- Install C# and .NET Test Explorer extensions
- Open the workspace
- Tests appear in Testing sidebar
- Run individually or all at once
CI/CD Integration
The setup works automatically in CI/CD pipelines thanks to InitialTargets:
# GitHub Actions example
- name: Build
run: dotnet build # Symlink created automatically
- name: Test
run: dotnet test
No manual symlink creation needed!
Comparison with Other Setups
| Aspect | Single Project | Multi-Project with ProjectReference | Multi-Project with Symlinks (This) |
|---|---|---|---|
| Resource Paths | Direct access | Need adjustment | Original paths preserved |
| Build Complexity | Simple | CS0436 warnings | Clean build |
| Assembly Count | One | Two | One (test only) |
| Disk Usage | Minimal | Minimal | Minimal |
| Git Complexity | Simple | Simple | Simple (symlinks ignored) |
| CI/CD Setup | Simple | Simple | Simple (InitialTargets) |
| Team Workflow | Mixed code | Clean separation | Clean separation |
Troubleshooting
Symlink Not Created
Problem: Build fails with "type or namespace not found" errors
Solution: Ensure InitialTargets is set in the project file:
<Project Sdk="Godot.NET.Sdk/4.5.1" InitialTargets="CreateSymlinks">
Windows Symlink Issues
Problem: "You do not have sufficient privilege"
Solutions:
- Enable Developer Mode (best option)
- Run as Administrator
- Let it fall back to junction (automatic)
Resources Not Found
Problem: "Cannot load resource: res://src/..."
Check:
- Symlink exists:
ls -la ExampleProject.Test/ - Manually create if needed:
ln -s ../ExampleProject/src src - Verify .gitignore isn't excluding needed files
Test Discovery Issues
- Ensure GdUnit4 test adapter is installed
- Check test class has
[TestSuite]attribute - Verify test methods have
[TestCase]attribute - Rebuild solution if tests aren't detected
Best Practices
- No ProjectReference: Don't add reference to main project
- Use InitialTargets: Ensures symlink exists before build
- Keep Tests Isolated: Don't modify shared resources in tests
- Test Data: Create test-specific data when needed
- Clean State: Reset any modified state after tests
- Documentation: Document any special setup requirements
Key Takeaways
- InitialTargets is crucial - runs before MSBuild file evaluation
- No ProjectReference needed - avoid duplicate compilation
- Symlinks preserve paths - resources load identically
- Single assembly - test project contains everything
- Works everywhere - local, CI/CD, all platforms