Skip to content

Parley Developer Testing

LordOfMyatar edited this page Feb 13, 2026 · 3 revisions

Parley Developer: Testing Guide

Guide for writing and running tests in Parley.


Table of Contents


Overview

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

Running Tests

All Tests

cd Parley
dotnet test

Specific Test Class

dotnet test --filter "FullyQualifiedName~DialogLoadingHeadlessTests"

Specific Test Method

dotnet test --filter "FullyQualifiedName=Parley.Tests.GUI.DialogLoadingHeadlessTests.CreateDialog_InitializesCorrectly"

With Verbose Output

dotnet test --logger "console;verbosity=detailed"

Test Types

Unit Tests (Service/Model Level)

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);
}

Avalonia Headless Tests (GUI Integration)

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

Async Tests

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);
}

Controller Testing

Controllers extracted during the Sprint 1-4 refactor have dedicated unit tests in Parley.Tests/Controllers/.

Test Files

Test Class Controller Under Test
EditMenuControllerTests EditMenuController
FileMenuControllerTests FileMenuController
QuestUIControllerTests QuestUIController
SpeakerVisualControllerTests SpeakerVisualController
TreeViewUIControllerTests TreeViewUIController

Controller Test Pattern

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);
}

What Controllers Test

  • Constructor validation (valid args, null args)
  • Method behavior with mock dependencies
  • State changes from UI interactions
  • Edge cases (no TreeView control, null selection, etc.)

Testing with DI and Mocks

Mock Service Implementations

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

Creating Mock Services

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
}

Testing Services with DI

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);
}

Writing New Tests

Step 1: Choose Test Type

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]

Step 2: Create Test File

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>

Step 3: Write Test

using Avalonia.Headless.XUnit;
using DialogEditor.Models;
using Xunit;

namespace Parley.Tests.GUI
{
    public class MyFeatureHeadlessTests
    {
        [AvaloniaFact]
        public void Scenario_ExpectedBehavior()
        {
            // Arrange
            // Act
            // Assert
        }
    }
}

Common Patterns

Dialog Creation

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;
}

Test Data Setup with Cleanup

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 */ }
        }
    }
}

Skip If File Missing

[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);
}

Privacy and Security

No Real Usernames

// Bad
[InlineData("C:\\Users\\RealName\\Documents\\file.txt")]

// Good
[InlineData("~\\Documents\\file.txt")]

Use Environment Variables

var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var testPath = Path.Combine(userProfile, "Parley", "test.dlg");

Sanitize Path Assertions

Assert.StartsWith("~", sanitized);
Assert.DoesNotContain(userProfile, sanitized);

Debugging Tests

Debug Single Test in IDE

  1. Set breakpoint in test method
  2. Right-click test in Test Explorer
  3. Select "Debug Test"

View Test Output

dotnet test --logger "console;verbosity=detailed"

Common Issues

TypeLoadException in Headless tests

  • Ensure Avalonia.Headless.XUnit version matches main Avalonia version

Test file not found

  • Use robust path resolution with Directory.GetCurrentDirectory()
  • Or skip test if file missing

CI/CD Integration

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)

Best Practices

DO

  • 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

DON'T

  • 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

Test Categories

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

See Also


Home | Index


Page freshness: 2026-02-07


Parley

Getting Started

User Guide

Features

Help


Manifest


Quartermaster


Fence

  • Fence - Merchant/Store Editor

Trebuchet


Shared Features


Developers

Parley Internals

Manifest Internals

Quartermaster Internals

Fence Internals

Trebuchet Internals

Radoub.UI


Radoub.Formats

Library

Low-Level Formats

High-Level Parsers


Legacy Bioware Docs

Original BioWare Aurora Engine file format specifications.

Core Formats

Object Blueprints

Module/Area Files

Reference


Index

Clone this wiki locally