Test Framework Integration Guide
June 26, 2026 ยท View on GitHub
skUnit is designed to be completely test-framework agnostic. This guide shows how to integrate skUnit with different test frameworks.
Core Principles
skUnit's core library (skUnit NuGet package) has no dependencies on any specific test framework. It only depends on:
Microsoft.Extensions.AI- For chat client abstractionsMicrosoft.SemanticKernel.Abstractions- For kernel abstractionsMarkdig- For markdown parsingSemanticValidation- For semantic assertions
This means you can use skUnit with any .NET testing framework.
Framework-Specific Patterns
xUnit
xUnit uses ITestOutputHelper for test output logging.
Basic Setup
using Xunit;
using Xunit.Abstractions;
using skUnit;
using skUnit.Scenarios;
public class ChatTests
{
private readonly IChatClient _chatClient;
private readonly ScenarioAssert _scenarioAssert;
public ChatTests(ITestOutputHelper output)
{
_chatClient = CreateChatClient(); // Your setup
_scenarioAssert = new ScenarioAssert(_chatClient, output.WriteLine);
}
[Fact]
public async Task SimpleTest()
{
var scenarios = await ChatScenario.ParseFromResourceAsync(
"MyProject.Scenarios.test.md",
typeof(ChatTests).Assembly);
await _scenarioAssert.PassAsync(scenarios, _chatClient);
}
}
Data-Driven Tests
[Theory]
[InlineData("Scenario1")]
[InlineData("Scenario2")]
public async Task TestScenarios(string scenarioName)
{
var scenarios = await ChatScenario.ParseFromResourceAsync(
$"MyProject.Scenarios.{scenarioName}.md",
typeof(ChatTests).Assembly);
await _scenarioAssert.PassAsync(scenarios, _chatClient);
}
MSTest
MSTest uses TestContext for test output logging and supports class-level initialization.
Basic Setup
using Microsoft.VisualStudio.TestTools.UnitTesting;
using skUnit;
using skUnit.Scenarios;
[TestClass]
public class ChatTests
{
private static IChatClient _chatClient = null!;
private static ScenarioAssert ScenarioAssert { get; set; } = null!;
public TestContext TestContext { get; set; } = null!;
[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
_chatClient = CreateChatClient(); // Your setup
ScenarioAssert = new ScenarioAssert(_chatClient, context.WriteLine);
}
[TestMethod]
public async Task SimpleTest()
{
var scenarios = await ChatScenario.ParseFromResourceAsync(
"MyProject.Scenarios.test.md",
typeof(ChatTests).Assembly);
await ScenarioAssert.PassAsync(scenarios, _chatClient);
}
}
Data-Driven Tests
[DataTestMethod]
[DataRow("Scenario1")]
[DataRow("Scenario2")]
public async Task TestScenarios(string scenarioName)
{
var scenarios = await ChatScenario.ParseFromResourceAsync(
$"MyProject.Scenarios.{scenarioName}.md",
typeof(ChatTests).Assembly);
await ScenarioAssert.PassAsync(scenarios, _chatClient);
}
NUnit
NUnit uses TestContext similar to MSTest but with slightly different patterns.
Basic Setup
using NUnit.Framework;
using skUnit;
using skUnit.Scenarios;
[TestFixture]
public class ChatTests
{
private IChatClient _chatClient = null!;
private ScenarioAssert _scenarioAssert = null!;
[OneTimeSetUp]
public void OneTimeSetUp()
{
_chatClient = CreateChatClient(); // Your setup
_scenarioAssert = new ScenarioAssert(_chatClient, TestContext.WriteLine);
}
[Test]
public async Task SimpleTest()
{
var scenarios = await ChatScenario.ParseFromResourceAsync(
"MyProject.Scenarios.test.md",
typeof(ChatTests).Assembly);
await _scenarioAssert.PassAsync(scenarios, _chatClient);
}
}
Data-Driven Tests
[TestCase("Scenario1")]
[TestCase("Scenario2")]
public async Task TestScenarios(string scenarioName)
{
var scenarios = await ChatScenario.ParseFromResourceAsync(
$"MyProject.Scenarios.{scenarioName}.md",
typeof(ChatTests).Assembly);
await _scenarioAssert.PassAsync(scenarios, _chatClient);
}
Logging Integration
The key difference between test frameworks is the logging mechanism:
| Framework | Logging Method | Pattern |
|---|---|---|
| xUnit | ITestOutputHelper.WriteLine | new ScenarioAssert(chatClient, output.WriteLine) |
| MSTest | TestContext.WriteLine | new ScenarioAssert(chatClient, TestContext.WriteLine) |
| NUnit | TestContext.WriteLine | new ScenarioAssert(chatClient, TestContext.WriteLine) |
For modern logging integration, you can also use ILogger<ScenarioAssert>:
// Modern approach with ILogger
var logger = loggerFactory.CreateLogger<ScenarioAssert>();
var scenarioAssert = new ScenarioAssert(chatClient, logger);
Project Configuration
Package References
All frameworks need these core packages:
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.7.1" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<!-- Add your preferred test framework -->
xUnit
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
MSTest
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
NUnit
<PackageReference Include="NUnit" Version="4.0.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
Embedded Resources
For all frameworks, embed your scenario files:
<ItemGroup>
<EmbeddedResource Include="Scenarios\*.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
Advanced Patterns
Shared Base Class (xUnit)
public abstract class SemanticTestBase : IDisposable
{
protected readonly ITestOutputHelper Output;
protected readonly IChatClient ChatClient;
protected readonly ScenarioAssert ScenarioAssert;
protected SemanticTestBase(ITestOutputHelper output)
{
Output = output;
ChatClient = CreateChatClient();
ScenarioAssert = new ScenarioAssert(ChatClient, output.WriteLine);
}
protected virtual IChatClient CreateChatClient() => /* your implementation */;
public virtual void Dispose() => /* cleanup */;
}
public class MyTests : SemanticTestBase
{
public MyTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task MyTest()
{
var scenarios = await ChatScenario.ParseFromResourceAsync(/* ... */);
await ScenarioAssert.PassAsync(scenarios);
}
}
Shared Base Class (MSTest)
[TestClass]
public abstract class SemanticTestBase
{
protected static IChatClient ChatClient = null!;
public TestContext TestContext { get; set; } = null!;
[ClassInitialize]
public static void BaseClassInitialize(TestContext context)
{
ChatClient = CreateChatClient();
}
protected static virtual IChatClient CreateChatClient() => /* your implementation */;
protected ScenarioAssert CreateScenarioAssert()
{
return new ScenarioAssert(ChatClient, TestContext.WriteLine);
}
}
[TestClass]
public class MyTests : SemanticTestBase
{
[TestMethod]
public async Task MyTest()
{
var scenarioAssert = CreateScenarioAssert();
var scenarios = await ChatScenario.ParseFromResourceAsync(/* ... */);
await scenarioAssert.PassAsync(scenarios);
}
}
Examples
See the demo projects for complete working examples:
demos/Demo.TddRepl/- xUnit exampledemos/Demo.MSTest/- MSTest exampledemos/Demo.TddMcp/- xUnit with MCP integration
Troubleshooting
Common Issues
- Scenario file not found: Ensure files are marked as
EmbeddedResource - No logging output: Check that you're passing the correct logging delegate
- API key not found: Configure user secrets or environment variables
Framework-Specific Issues
MSTest
- Ensure
TestContextproperty is properly initialized - Use
[ClassInitialize]for shared setup, not constructor - Remember
[DataTestMethod]requires[DataRow]attributes
xUnit
- Constructor is called for each test, not shared
- Use
IClassFixture<T>for shared setup across tests [Theory]requires[InlineData]or other data attributes
NUnit
- Use
[OneTimeSetUp]for shared setup [TestCase]is similar to xUnit's[InlineData]- TestContext works similarly to MSTest
This guide proves that skUnit truly works with any test framework - choose the one your team prefers!