-
Notifications
You must be signed in to change notification settings - Fork 0
Parley Developer Testing
Guide for writing and running tests in Parley.
- Overview
- Running Tests
- Test Types
- Controller Testing
- Testing with DI and Mocks
- Writing New Tests
- Common Patterns
- Privacy and Security
- Debugging Tests
- CI/CD Integration
- Best Practices
Parley uses xUnit for unit testing with Avalonia.Headless for GUI integration tests.
Current Coverage (as of February 2026):
- 671 tests across 71 test files
- Parser/GFF, Copy/Paste, Delete, Undo/Redo, Orphan Detection
- Controller unit tests (5 controller test classes)
- Plugin Security (sandbox, permissions, rate limiting)
- Settings, Logging, Script Services
- Flowchart conversion
- GUI Headless tests
cd Parley
dotnet testdotnet test --filter "FullyQualifiedName~DialogLoadingHeadlessTests"dotnet test --filter "FullyQualifiedName=Parley.Tests.GUI.DialogLoadingHeadlessTests.CreateDialog_InitializesCorrectly"dotnet test --logger "console;verbosity=detailed"When to use: Testing business logic, data models, services
Attribute: [Fact] or [Theory]
[Fact]
public void SanitizePath_ReplacesUserProfileWithTilde()
{
// Arrange
var testPath = Path.Combine(_testUserProfile, "Parley", "Logs", "test.log");
// Act
var sanitized = UnifiedLogger.SanitizePath(testPath);
// Assert
Assert.StartsWith("~", sanitized);
Assert.DoesNotContain(_testUserProfile, sanitized);
}When to use: Testing UI workflows, dialog loading, node creation
Attribute: [AvaloniaFact] or [AvaloniaTheory]
[AvaloniaFact]
public void CreateDialog_InitializesCorrectly()
{
var dialog = new Dialog();
Assert.NotNull(dialog);
Assert.NotNull(dialog.Entries);
Assert.Empty(dialog.Entries);
}Key Differences:
- Use
[AvaloniaFact]instead of[Fact] - Tests run in headless mode (no actual UI window)
- Avalonia application initialized automatically
When to use: Testing file I/O, async workflows
[AvaloniaFact]
public async Task LoadDialog_SimpleFile_ParsesSuccessfully()
{
var testFile = Path.Combine(_testFilesPath, "test1_link.dlg");
var dialogService = new DialogFileService();
var dialog = await dialogService.LoadFromFileAsync(testFile);
Assert.NotNull(dialog);
Assert.NotEmpty(dialog.Entries);
}Controllers extracted during the Sprint 1-4 refactor have dedicated unit tests in Parley.Tests/Controllers/.
| Test Class | Controller Under Test |
|---|---|
| EditMenuControllerTests | EditMenuController |
| FileMenuControllerTests | FileMenuController |
| QuestUIControllerTests | QuestUIController |
| SpeakerVisualControllerTests | SpeakerVisualController |
| TreeViewUIControllerTests | TreeViewUIController |
Controllers accept dependencies via constructor, making them testable with mocks:
[Fact]
public void Constructor_ValidArgs_CreatesInstance()
{
// Arrange
var mockWindow = new MockWindow();
var mockSettings = new MockSettingsService();
// Act
var controller = new SpeakerVisualController(
mockWindow,
mockSettings,
() => false); // isPopulatingProperties callback
// Assert
Assert.NotNull(controller);
}- Constructor validation (valid args, null args)
- Method behavior with mock dependencies
- State changes from UI interactions
- Edge cases (no TreeView control, null selection, etc.)
Mock implementations exist for DI-registered service interfaces, enabling controller and integration testing without real services:
| Mock | Interface |
|---|---|
| MockSettingsService | ISettingsService |
| MockDialogContextService | IDialogContextService |
| MockScriptService | IScriptService |
| MockPortraitService | IPortraitService |
| MockJournalService | IJournalService |
Mock services implement the interface with minimal behavior:
public class MockSettingsService : ISettingsService
{
public string ModulePath { get; set; } = "";
public string BaseGamePath { get; set; } = "";
// ... minimal property implementations
}For integration tests that need the full DI container:
[Fact]
public void ServiceResolution_AllCoreServices_Resolve()
{
var services = new ServiceCollection();
services.AddSingleton<ISettingsService>(new MockSettingsService());
// ... register test services
var provider = services.BuildServiceProvider();
var settings = provider.GetRequiredService<ISettingsService>();
Assert.NotNull(settings);
}| Scenario | Test Type | Attribute |
|---|---|---|
| Business logic | Unit Test | [Fact] |
| Controller behavior | Unit Test | [Fact] |
| Dialog loading | Headless Test | [AvaloniaFact] |
| File operations | Async Test |
[Fact] + async Task
|
| UI interaction | Headless Test | [AvaloniaFact] |
Location:
-
Parley.Tests/- Standard service/model tests -
Parley.Tests/Controllers/- Controller tests -
Parley.Tests/GUI/- Headless tests -
Parley.Tests/Security/- Security tests
Naming Convention:
- Class:
<Feature>Tests.cs - Method:
<Scenario>_<Expected>
using Avalonia.Headless.XUnit;
using DialogEditor.Models;
using Xunit;
namespace Parley.Tests.GUI
{
public class MyFeatureHeadlessTests
{
[AvaloniaFact]
public void Scenario_ExpectedBehavior()
{
// Arrange
// Act
// Assert
}
}
}private Dialog CreateSimpleDialog()
{
var dialog = new Dialog();
var entry = dialog.CreateNode(DialogNodeType.Entry);
entry!.Text.Add(0, "Test Entry");
dialog.AddNodeInternal(entry, DialogNodeType.Entry);
var entryPtr = dialog.CreatePtr();
entryPtr!.Type = DialogNodeType.Entry;
entryPtr.Node = entry;
entryPtr.Index = 0;
dialog.Starts.Add(entryPtr);
return dialog;
}public class MyTests : IDisposable
{
private readonly string _testDirectory;
public MyTests()
{
_testDirectory = Path.Combine(
Path.GetTempPath(),
$"ParleyTests_{Guid.NewGuid()}"
);
Directory.CreateDirectory(_testDirectory);
}
public void Dispose()
{
if (Directory.Exists(_testDirectory))
{
try { Directory.Delete(_testDirectory, true); }
catch { /* Ignore cleanup errors */ }
}
}
}[AvaloniaFact]
public async Task LoadDialog_File_ParsesCorrectly()
{
var testFile = Path.Combine(_testFilesPath, "test.dlg");
if (!File.Exists(testFile))
return; // Skip if test file missing
var dialog = await dialogService.LoadFromFileAsync(testFile);
Assert.NotNull(dialog);
}// Bad
[InlineData("C:\\Users\\RealName\\Documents\\file.txt")]
// Good
[InlineData("~\\Documents\\file.txt")]var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var testPath = Path.Combine(userProfile, "Parley", "test.dlg");Assert.StartsWith("~", sanitized);
Assert.DoesNotContain(userProfile, sanitized);- Set breakpoint in test method
- Right-click test in Test Explorer
- Select "Debug Test"
dotnet test --logger "console;verbosity=detailed"TypeLoadException in Headless tests
- Ensure
Avalonia.Headless.XUnitversion matches main Avalonia version
Test file not found
- Use robust path resolution with
Directory.GetCurrentDirectory() - Or skip test if file missing
Tests run automatically on PR creation via GitHub Actions:
- name: Run Tests
run: dotnet test --logger "console;verbosity=minimal"Requirements:
- All tests must pass before PR merge
- Test execution time < 30s for full suite
- No flaky tests (tests that randomly fail)
- Write tests before fixing bugs (TDD for bug fixes)
- Keep tests fast (< 2s per test)
- Use descriptive test names
- Test edge cases and error conditions
- Clean up test data in
Dispose() - Use Arrange/Act/Assert pattern
- Use mock services for controller tests
- Test controllers independently of UI
- Test implementation details (test behavior, not internals)
- Use real user paths/names in test data
- Create dependencies between tests
- Skip assertions (every test must assert something)
- Test framework code (test your logic, not xUnit/Avalonia)
- Instantiate real services when mocks suffice
| Category | Files | Description |
|---|---|---|
| Parser/GFF |
GffParserTests, BasicParserTests
|
Binary format parsing |
| Copy/Paste |
CopyPasteTests, DialogClipboardServiceTests
|
Clipboard operations |
| Delete |
DeleteOperationTests, DeleteDeepTreeTests
|
Node deletion |
| Orphan |
OrphanNodeTests, OrphanNodeCleanupTests
|
Orphan detection |
| Undo/Redo |
UndoRedoTests, UndoStackLimitTests
|
State management |
| Controllers |
*ControllerTests (5 files) |
UI controller logic |
| Security | Security/*Tests |
Plugin sandbox, permissions |
| Flowchart |
Flowchart*Tests, DialogToFlowchartConverterTests
|
Visual conversion |
| GUI | GUI/*Tests |
Headless UI tests |
| Services | Various *ServiceTests
|
Service layer |
- Parley-Developer-Architecture - Code organization
- Parley-Developer-Delete-Behavior - Delete operation details
Page freshness: 2026-02-07
Getting Started
User Guide
Features
Help
- Manifest - Journal Editor
- Quartermaster - Creature/Inventory Editor (Coming Spring 2026)
- Fence - Merchant/Store Editor
- Trebuchet - Radoub Launcher
- Spell Check - Dictionary-based spell checking
- Token System - Dialog tokens and custom colors
Parley Internals
Manifest Internals
Quartermaster Internals
Fence Internals
Trebuchet Internals
Radoub.UI
Library
Low-Level Formats
High-Level Parsers
- JRL Format (.jrl)
- UTI Format (.uti) - Item blueprints
- UTC Format (.utc) - Creature blueprints
- UTM Format (.utm) - Store blueprints
- BIC Format (.bic) - Player characters
Original BioWare Aurora Engine file format specifications.
Core Formats
- GFF Format - Generic File Format
- KEY/BIF Format - Resource archives
- ERF Format - Encapsulated resources
- TLK Format - Talk tables
- 2DA Format - Data tables
- Localized Strings
- Common GFF Structs
Object Blueprints
- Creature Format (.utc)
- Item Format (.uti)
- Store Format (.utm)
- Door/Placeable (.utd/.utp)
- Encounter Format (.ute)
- Sound Object (.uts)
- Trigger Format (.utt)
- Waypoint Format (.utw)
Module/Area Files
- Conversation Format (.dlg)
- Journal Format (.jrl)
- Area File Format (.are/.git/.gic)
- Module Info (.ifo)
- Faction Format (.fac)
- Palette/ITP Format (.itp)
- SSF Format - Sound sets
Reference