skUnit 0.58.0-beta

This is a prerelease version of skUnit.
dotnet add package skUnit --version 0.58.0-beta
                    
NuGet\Install-Package skUnit -Version 0.58.0-beta
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="skUnit" Version="0.58.0-beta" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="skUnit" Version="0.58.0-beta" />
                    
Directory.Packages.props
<PackageReference Include="skUnit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add skUnit --version 0.58.0-beta
                    
#r "nuget: skUnit, 0.58.0-beta"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package skUnit@0.58.0-beta
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=skUnit&version=0.58.0-beta&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=skUnit&version=0.58.0-beta&prerelease
                    
Install as a Cake Tool

skUnit

Build and Deploy NuGet version (skUnit) NuGet downloads

skUnit is a semantic testing framework for .NET that makes it easy to test AI-powered applications using simple, readable Markdown scenarios.

Test anything that talks to AI:

  • ๐Ÿค– IChatClient implementations (Azure OpenAI, OpenAI, Anthropic, etc.)
  • ๐Ÿง  SemanticKernel applications and plugins
  • ๐Ÿ”ง MCP (Model Context Protocol) servers
  • ๐Ÿ› ๏ธ Custom AI integrations

Write your tests in Markdown, run them with any test framework (xUnit, NUnit, MSTest), and get live, readable results.

โšก Quick Start

Here's a simple test scenario in Markdown:

# SCENARIO Mountain Chat

## [USER]
What is the tallest mountain?

## [ASSISTANT]
The tallest mountain is Everest! (OPTIONAL)

### ASSERT SemanticCondition
It mentions Mount Everest.

And here's how to test it with just a few lines of C#:

[Fact]
public async Task SimpleTest()
{
    var markdown = File.ReadAllText("mountain-chat.md");
    var scenarios = ChatScenario.LoadFromText(markdown);

    await ScenarioRunner.RunAsync(scenarios, systemUnderTestClient);
}

Note that in this example, the agent message is just for clarity and is not being used and is optional. So the following test scenario is equivalent:

## [USER]
What is the tallest mountain?

## [ASSISTANT]

### ASSERT SemanticCondition
It mentions Mount Everest.

That's it! โœจ skUnit handles the conversation, calls your AI, and verifies the response makes sense.

๐ŸŽฏ Key Features

1. Start Simple: Basic Chat Scenarios

Test single interactions with basic checks:

## [USER]
Is Everest a mountain or a Tree?

## [ASSISTANT]

### ASSERT ContainsAny
mountain

### ASSERT SemanticCondition
It mentions the mountain

2. Level Up: JSON Validation

Test structured responses with powerful JSON assertions:

# SCENARIO User Info

## [USER]
Give me the most expensive product info as a JSON like this:
{"id": 12, "title": "The product", "price": 0, "description": "the description of the product"}

## [ASSISTANT]
{"id": 12, "title": "Surface Studio 2", "price": 3000, "description: "It is a very high-quality laptop"}

### ASSERT JsonCheck
{
  "id": ["NotEmpty"],
  "title": ["Contains", "Surface"],
  "price": ["Equal", 3000],
  "description": ["SemanticCondition", "It mentions the quality of the laptop."]
}

3. Advanced: Function Call Testing

Verify your AI calls the right functions (MCP maybe) with the right parameters:

# SCENARIO Time Query

## [USER]
What time is it?

## [ASSISTANT]
It's currently 2:30 PM

### ASSERT FunctionCall
{
  "function_name": "get_current_time"
}

Even you can assert the called parameters:

ASSERT FunctionCall

{ "function_name": "GetFoodMenu", "arguments": { "mood": ["Equals", "Happy"] } }

4. Multi-Turn Conversations

Test complex conversations with multiple exchanges:

# SCENARIO Height Discussion

## [USER]
Is Eiffel tall?

## [ASSISTANT]
Yes it is

### ASSERT SemanticCondition
It agrees that the Eiffel Tower is tall or expresses a positive sentiment.

## [USER]
What about Everest?

## [ASSISTANT]
Yes it is tall too

### ASSERT SemanticCondition
It agrees that Everest is tall or expresses a positive sentiment.

skUnit Chat Scenario Structure

Each scenario can contain multiple sub-scenarios (conversation turns), and each response can have multiple ASSERT statements to verify different aspects of the AI's behavior.

5. Readable Markdown Scenarios

Your test scenarios are just valid Markdown files - easy to read, write, and review:

Markdown Scenario Example

6. Live Test Results

Watch your tests run in real-time with beautiful, readable output:

Live Test Results

7. MCP Server Testing

Test Model Context Protocol servers to ensure your tools work correctly:

# SCENARIO MCP Time Server

## [USER]
What time is it?

## [ASSISTANT]
It's currently 2:30 PM PST

### ASSERT FunctionCall
{
  "function_name": "current_time"
}

### ASSERT SemanticCondition
It mentions a specific time
// Setup MCP server testing
var mcp = await McpClientFactory.CreateAsync(clientTransport);
var tools = await mcp.ListToolsAsync();

var chatClient = new ChatClientBuilder(baseChatClient)
    .ConfigureOptions(options => options.Tools = tools.ToArray())
    .UseFunctionInvocation()
    .Build();

// In your test class constructor:
var assertionClient = /* assertion/evaluation model */;
ScenarioRunner = new ChatScenarioRunner(assertionClient);

// In your test:
await ScenarioRunner.RunAsync(scenarios, chatClient);

๐Ÿš€ Installation & Setup

1. Install the Package

dotnet add package skUnit

2. Basic Setup

public class MyChatTests
{
    private readonly ChatScenarioRunner _scenarioRunner;
    private readonly IChatClient _chatClient;

    public MyChatTests(ITestOutputHelper output)
    {
        // Configure your AI client (Azure OpenAI, OpenAI, etc.)
        _chatClient = new AzureOpenAIClient(endpoint, credential)
            .GetChatClient(deploymentName)
            .AsIChatClient();
            
        _scenarioRunner = new ChatScenarioRunner(_chatClient, output.WriteLine);
    }

    [Fact]
    public async Task TestChat()
    {
        var markdown = File.ReadAllText("scenario.md");
        var scenarios = ChatScenario.LoadFromText(markdown);
        
        await _scenarioRunner.RunAsync(scenarios, _chatClient);
    }
}

3. Configuration

Set up your AI provider credentials:

{
  "AzureOpenAI_ApiKey": "your-api-key",
  "AzureOpenAI_Endpoint": "https://your-endpoint.openai.azure.com/",
  "AzureOpenAI_Deployment": "your-deployment-name"
}

๐Ÿงช Testing Multiple MCP Servers

Test complex scenarios involving multiple MCP servers working together:

// Combine multiple MCP servers
var timeServer = await McpClientFactory.CreateAsync(timeTransport);
var weatherServer = await McpClientFactory.CreateAsync(weatherTransport);

var allTools = new List<AITool>();
allTools.AddRange(await timeServer.ListToolsAsync());
allTools.AddRange(await weatherServer.ListToolsAsync());

var chatClient = new ChatClientBuilder(baseChatClient)
    .ConfigureOptions(options => options.Tools = allTools.ToArray())
    .UseFunctionInvocation()
    .Build();

๐Ÿงช Works with Any Test Framework

skUnit is completely test-framework agnostic! Here's the same test with different frameworks:

xUnit

public class GreetingTests
{
    private readonly ChatScenarioRunner ScenarioRunner;
    private readonly IChatClient systemUnderTestClient;

    public GreetingTests()
    {
        var assertionClient = /* assertion/evaluation model */;
        systemUnderTestClient = /* system under test model */;
        ScenarioRunner = new ChatScenarioRunner(assertionClient);
    }

    [Fact]
    public async Task TestGreeting()
    {
        var markdown = File.ReadAllText("greeting.md");
        var scenarios = ChatScenario.LoadFromText(markdown);

        await ScenarioRunner.RunAsync(scenarios, systemUnderTestClient);
    }
}

MSTest

public class GreetingTests : TestClass
{
    private readonly ChatScenarioRunner ScenarioRunner;
    private readonly IChatClient systemUnderTestClient;

    public GreetingTests()
    {
        var assertionClient = /* assertion/evaluation model */;
        systemUnderTestClient = /* system under test model */;
        ScenarioRunner = new ChatScenarioRunner(assertionClient, TestContext.WriteLine);
    }

    [TestMethod]
    public async Task TestGreeting()
    {
        var scenarios = await ChatScenario.LoadFromResourceAsync(
            "MyProject.Scenarios.greeting.md", 
            typeof(GreetingTests).Assembly);
            
        await ScenarioRunner.RunAsync(scenarios, systemUnderTestClient);
    }
}

NUnit

public class GreetingTests
{
    private readonly ChatScenarioRunner ScenarioRunner;
    private readonly IChatClient systemUnderTestClient;

    public GreetingTests()
    {
        var assertionClient = /* assertion/evaluation model */;
        systemUnderTestClient = /* system under test model */;
        ScenarioRunner = new ChatScenarioRunner(assertionClient, TestContext.WriteLine);
    }

    [Test]
    public async Task TestGreeting()
    {
        var markdown = File.ReadAllText("greeting.md");
        var scenarios = ChatScenario.LoadFromText(markdown);

        await ScenarioRunner.RunAsync(scenarios, systemUnderTestClient);
    }
}

The core difference is just the logging integration - use TestContext.WriteLine for MSTest, ITestOutputHelper.WriteLine for xUnit, or TestContext.WriteLine for NUnit. Both patterns show:

  • Assertion Client: Created once in the constructor for semantic evaluations
  • System Under Test Client: The client whose behavior you're testing, passed to RunAsync

๐Ÿ“š Documentation

๐Ÿ“‹ Requirements

  • .NET 8.0 or higher
  • AI Provider (Azure OpenAI, OpenAI, Anthropic, etc.) for semantic assertions
  • Test Framework (xUnit, NUnit, MSTest - your choice!)

๐Ÿ”’ Advanced: Mitigating Hallucinations with ScenarioRunOptions

LLM outputs can vary between runs. A single spurious response shouldn't fail your build if the model normally behaves correctly.

Use ScenarioRunOptions to execute each scenario multiple times and require only a percentage to pass. This adds statistical robustness without eliminating genuine regressions.

var options = new ScenarioRunOptions
{
    TotalRuns = 3,        // Run the whole scenario three times
    MinSuccessRate = 0.67 // At least 2 of 3 runs must pass
};

// In your test class constructor:
var assertionClient = /* assertion/evaluation model */;
ScenarioRunner = new ChatScenarioRunner(assertionClient);

// In your test:
await ScenarioRunner.RunAsync(scenarios, systemUnderTestClient, options: options);

Recommended starting points:

  • Deterministic / low-temp prompts: TotalRuns = 1, MinSuccessRate = 1.0
  • Function / tool invocation: TotalRuns = 3, MinSuccessRate = 0.67
  • Creative generation: TotalRuns = 5, MinSuccessRate = 0.6
  • Critical CI gating: TotalRuns = 5, MinSuccessRate = 0.8

Failure message example:

Only 40% of rounds passed, which is below the required success rate of 80%

Indicates a systematic issue (not just randomness) โ€“ investigate prompt, model settings, or assertions.

See full guide: Scenario Run Options

๐Ÿค Contributing

We welcome contributions! Check out our issues or submit a PR.

โญ Examples

Check out the /demos folder for complete examples:

  • Demo.TddRepl - Interactive chat application testing
  • Demo.TddMcp - MCP server integration testing
  • Demo.TddShop - Complex e-commerce chat scenarios

Start testing your AI applications with confidence! ๐ŸŽฏ

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.58.0-beta 33 9/1/2025
0.57.0-beta 33 9/1/2025
0.56.0-beta 55 8/31/2025
0.55.0-beta 144 8/29/2025
0.54.0-beta 44 8/16/2025
0.53.0-beta 113 8/11/2025
0.52.0-beta 1,254 5/13/2025
0.51.0-beta 87 5/2/2025
0.50.0-beta 183 4/15/2025
0.49.0-beta 165 4/15/2025
0.47.0-beta 169 4/15/2025
0.43.0-beta 1,267 4/14/2025
0.42.0-beta 173 4/14/2025
0.40.0-beta 828 3/14/2025
0.39.0-beta 337 3/2/2025
0.38.0-beta 352 1/13/2025
0.37.0-beta 60 1/9/2025
0.35.0-beta 68 1/8/2025
0.34.0-beta 132 11/26/2024
0.33.0-beta 121 10/20/2024
0.32.0-beta 105 9/15/2024
0.31.0-beta 83 9/14/2024
0.30.0-beta 85 9/14/2024
0.29.0-beta 333 1/4/2024
0.28.0-beta 101 1/2/2024
0.27.0-beta 146 1/2/2024
0.26.0-beta 104 1/2/2024
0.25.0-beta 114 12/30/2023
0.24.0-beta 98 12/29/2023
0.23.0-beta 106 12/28/2023
0.22.0-beta 102 12/28/2023
0.21.0-beta 93 12/28/2023
0.20.0-beta 108 12/28/2023
0.19.0-beta 87 12/27/2023
0.18.0-beta 96 12/27/2023
0.16.0-beta 92 12/27/2023
0.15.0-beta 106 12/27/2023
0.14.0-beta 107 12/26/2023
0.13.0-beta 99 12/26/2023
0.12.0-beta 96 12/26/2023
0.11.0-beta 104 12/26/2023
0.10.0-beta 95 12/25/2023
0.9.0-beta 102 12/25/2023
0.8.0-beta 96 12/25/2023
0.7.0-beta 102 12/25/2023
0.6.0-beta 97 12/25/2023
0.5.0-beta 100 12/24/2023
0.4.0-beta 89 12/24/2023
0.3.0-beta 106 12/24/2023
0.1.0-beta 95 12/24/2023