Unit Testing Xians Router Capabilities¶
This guide covers how to unit test capabilities that use the Xians semantic router framework.
Test Class Setup¶
Required Imports¶
using AgentTools;
using DotNetEnv;
using Server;
using System.Reflection;
using XiansAi.Flow;
using XiansAi.Flow.Router;
Environment Variables & Context Setup¶
public class MyCapabilitiesTests
{
/// <summary>
/// Run with: dotnet test --filter "FullyQualifiedName~MyCapabilitiesTests"
/// </summary>
static MyCapabilitiesTests()
{
// Load environment variables once for the entire test suite
var assemblyLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var projectRoot = Path.GetFullPath(Path.Combine(assemblyLocation!, "..", "..", "..", ".."));
var envPath = Path.Combine(projectRoot, ".env");
Env.Load(envPath);
// Set agent context for testing
AgentContext.SetLocalContext("test-user", "test-tenant:test-agent:test-workflow");
AgentContext.RouterOptions = new RouterOptions {
ProviderName = "openai",
ApiKey = Env.GetString("OPENAI_API_KEY"),
ModelName = "gpt-4o-mini",
};
// Create agent and add capabilities
var agent = new Agent("TestAgent");
// Add web bot with capabilities
var webBot = agent.AddBot<WebBot>(numberOfWorkers: 3);
webBot.AddCapabilities(typeof(MyCapability));
webBot.AddCapabilities(typeof(FirecrawlCapability));
webBot.AddCapabilities(typeof(GoogleSearchCapability));
webBot.AddKernelModifier(new PlayWrightMCP());
// Add reporter bot with kernel modifiers
var reporterBot = agent.AddBot<ReporterBot>();
reporterBot.AddKernelModifier(new MicrosoftO365MCP());
reporterBot.AddKernelModifier(new PdfGeneratorMCP());
}
}
Environment Configuration¶
Required .env Variables¶
Benefits of LOCAL_KNOWLEDGE_FOLDER is that it reduces external server dependencies during testing. See Local Development for more details.
Test Categories¶
Unit Tests¶
/// <summary>
/// Run with: dotnet test --filter "FullyQualifiedName~MyMethod_WithValidInput_ShouldReturnExpectedOutput"
/// </summary>
[Theory]
[Trait("Category", "Unit")]
[InlineData("test input", "expected output")]
public async Task MyMethod_WithValidInput_ShouldReturnExpectedOutput(string input, string expected)
{
// Act
var result = await new MyCapabilities().MyMethod(input);
// Assert
Assert.Equal(expected, result);
}
Integration Tests¶
Integration tests require API keys and network access to external services:
/// <summary>
/// Run with: dotnet test --filter "FullyQualifiedName~MyIntegrationMethod_WithValidInput_ShouldReturnExpectedResult"
/// </summary>
[Theory]
[Trait("Category", "Integration")]
[InlineData("https://example.com", "Expected Company")]
public async Task MyIntegrationMethod_WithValidInput_ShouldReturnExpectedResult(string url, string expectedCompany)
{
// Act
var result = await new MyCapabilities().MyIntegrationMethod(new Uri(url));
// Assert
Assert.Equal(expectedCompany, result);
// Additional assertions for integration tests
Assert.NotNull(result);
Assert.NotEmpty(result);
Console.WriteLine($"Result: {result}");
}
Testing Agent-to-Agent Communication¶
Agent2Agent chat message passing works seamlessly in unit tests when both agents are in the same .NET process. No special configuration needed.
public class MyCapabilities
{
[Capability("Determine if a company is a software product company (ISV) and is a small or medium enterprise (SME)")]
[Parameter("companyWebsite", "Website of the company to determine if it is a software product company (ISV)")]
[Returns("True if the company is a software product company (ISV), false otherwise")]
public async Task<bool> IsSMEProductCompany(Uri companyWebsite)
{
var instruction = @$"
Url: {companyWebsite}
Read the content of the above url and return the content in markdown format.
Return the content in English.
Do not return any other text.
First try/retry with scraping tools. If fails try web automation tools. If still fails return text 'ERROR: <reason>'.
";
// Call WebBot Agent to extract the content
var siteContent = await MessageHub.Agent2Agent.SendChat(typeof(WebBot), instruction);
if (string.IsNullOrEmpty(siteContent.Text) || siteContent.Text.StartsWith("ERROR"))
{
throw new Exception($"Error occurred in reading page content from: {companyWebsite}: {siteContent.Text}");
}
return await IsSMEProductCompany(siteContent.Text);
}
}
Test Cleanup and File Management¶
For tests that create temporary files, always use proper cleanup:
[Theory]
[Trait("Category", "Unit")]
[InlineData("# Test Content")]
public async Task SaveAsMarkdown_WithValidContent_ShouldCreateFile(string content)
{
string? filePath = null;
try
{
// Act
filePath = await new MyCapabilities().SaveAsMarkdown(content);
// Assert
Assert.NotNull(filePath);
Assert.True(File.Exists(filePath));
Assert.EndsWith(".md", filePath);
var savedContent = await File.ReadAllTextAsync(filePath);
Assert.Equal(content, savedContent);
}
finally
{
// Clean up: Delete the created file
if (filePath != null && File.Exists(filePath))
{
File.Delete(filePath);
}
}
}
Running Tests¶
# Run entire test class
dotnet test --filter "FullyQualifiedName~MyCapabilitiesTests"
# Run specific test method
dotnet test --filter "FullyQualifiedName~MyMethod_WithValidInput_ShouldReturnExpectedOutput"
# Run by category
dotnet test --filter "Category=Unit"
dotnet test --filter "Category=Integration"
# Run with console output for debugging
dotnet test --filter "FullyQualifiedName~MyMethod_WithValidInput" --logger "console;verbosity=detailed"
Key Changes from Previous Setup¶
- RouterOptions moved to AgentContext: Router options are now set globally on
AgentContext.RouterOptions
instead of being passed to capability constructors - Static constructor setup: All agent and bot initialization happens once in the static constructor
- No constructor parameters: Capability classes no longer need RouterOptions constructor parameters
- Enhanced bot setup: More comprehensive bot configuration with multiple capabilities and kernel modifiers
- Better test organization: Clear separation between Unit and Integration test categories with appropriate traits