Testing Framework Integration Guide
This guide provides detailed integration instructions and best practices for using SMock with popular .NET testing frameworks.
Table of Contents
- Framework Compatibility
- NUnit Integration
- xUnit Integration
- MSTest Integration
- SpecFlow Integration
- Custom Test Frameworks
- CI/CD Integration
Framework Compatibility
SMock works seamlessly with all major .NET testing frameworks. Here's the compatibility matrix:
| Framework | Version | Support Level | Special Features |
|---|---|---|---|
| NUnit | 3.0+ | ✅ Full | Parallel execution, custom attributes |
| xUnit | 2.0+ | ✅ Full | Fact/Theory patterns, collection fixtures |
| MSTest | 2.0+ | ✅ Full | DataRow testing, deployment items |
| SpecFlow | 3.0+ | ✅ Full | BDD scenarios, step definitions |
| Fixie | 2.0+ | ✅ Full | Convention-based testing |
| Expecto | 9.0+ | ✅ Full | F# functional testing |
NUnit Integration
Basic Setup
using NUnit.Framework;
using StaticMock;
[TestFixture]
public class NUnitSMockTests
{~~~~~~~~
[Test]
public void Basic_SMock_Test()
{
using var mock = Mock.Setup(() => DateTime.Now)
.Returns(new DateTime(2024, 1, 1));
var result = DateTime.Now;
Assert.AreEqual(new DateTime(2024, 1, 1), result);
}
}
NUnit Parameterized Tests
[TestFixture]
public class ParameterizedNUnitTests
{
[Test]
[TestCase("file1.txt", "content1")]
[TestCase("file2.txt", "content2")]
[TestCase("file3.txt", "content3")]
public void Parameterized_File_Mock_Test(string fileName, string expectedContent)
{
using var mock = Mock.Setup(() => File.ReadAllText(fileName))
.Returns(expectedContent);
var processor = new FileProcessor();
var result = processor.ProcessFile(fileName);
Assert.AreEqual(expectedContent.ToUpper(), result);
}
[Test]
[TestCaseSource(nameof(GetTestData))]
public void TestCaseSource_Example(TestData data)
{
using var mock = Mock.Setup(() => TestService.GetValue(data.Input))
.Returns(data.ExpectedOutput);
var result = TestService.GetValue(data.Input);
Assert.AreEqual(data.ExpectedOutput, result);
}
private static IEnumerable<TestData> GetTestData()
{
yield return new TestData { Input = "test1", ExpectedOutput = "result1" };
yield return new TestData { Input = "test2", ExpectedOutput = "result2" };
yield return new TestData { Input = "test3", ExpectedOutput = "result3" };
}
public class TestData
{
public string Input { get; set; }
public string ExpectedOutput { get; set; }
}
}
NUnit Parallel Execution
[TestFixture]
[Parallelizable(ParallelScope.Self)]
public class ParallelNUnitTests
{
[Test]
[Parallelizable]
public void Parallel_Test_1()
{
using var mock = Mock.Setup(() => TestClass.Method("parallel_1"))
.Returns("result_1");
var result = TestClass.Method("parallel_1");
Assert.AreEqual("result_1", result);
}
[Test]
[Parallelizable]
public void Parallel_Test_2()
{
using var mock = Mock.Setup(() => TestClass.Method("parallel_2"))
.Returns("result_2");
var result = TestClass.Method("parallel_2");
Assert.AreEqual("result_2", result);
}
}
NUnit One-Time Setup with SMock
[TestFixture]
public class OneTimeSetupTests
{
private static IDisposable _globalDateMock;
[OneTimeSetUp]
public void GlobalSetup()
{
// Setup mocks that persist across all tests in this fixture
_globalDateMock = Mock.Setup(() => Environment.MachineName)
.Returns("TEST_MACHINE");
}
[OneTimeTearDown]
public void GlobalTearDown()
{
// Clean up global mocks
_globalDateMock?.Dispose();
}
[Test]
public void Test_With_Global_Mock_1()
{
// This test uses the globally set up mock
var machineName = Environment.MachineName;
Assert.AreEqual("TEST_MACHINE", machineName);
// Add test-specific mocks
using var dateMock = Mock.Setup(() => DateTime.Now)
.Returns(new DateTime(2024, 1, 1));
var service = new MachineAwareService();
var result = service.GetMachineTimeStamp();
Assert.IsNotNull(result);
}
[Test]
public void Test_With_Global_Mock_2()
{
// This test also benefits from the global mock
var machineName = Environment.MachineName;
Assert.AreEqual("TEST_MACHINE", machineName);
}
}
xUnit Integration
Basic xUnit Setup
using Xunit;
using StaticMock;
public class XUnitSMockTests
{
[Fact]
public void Basic_SMock_Fact()
{
using var mock = Mock.Setup(() => DateTime.Now)
.Returns(new DateTime(2024, 1, 1));
var result = DateTime.Now;
Assert.Equal(new DateTime(2024, 1, 1), result);
}
[Theory]
[InlineData("input1", "output1")]
[InlineData("input2", "output2")]
[InlineData("input3", "output3")]
public void Theory_With_SMock(string input, string expectedOutput)
{
using var mock = Mock.Setup(() => TestService.Transform(input))
.Returns(expectedOutput);
var result = TestService.Transform(input);
Assert.Equal(expectedOutput, result);
}
}
xUnit Collection Fixtures
// Collection fixture for shared mocks across test classes
public class SharedMockFixture : IDisposable
{
public IDisposable DateTimeMock { get; private set; }
public IDisposable EnvironmentMock { get; private set; }
public SharedMockFixture()
{
DateTimeMock = Mock.Setup(() => DateTime.UtcNow)
.Returns(new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc));
EnvironmentMock = Mock.Setup(() => Environment.UserName)
.Returns("TEST_USER");
}
public void Dispose()
{
DateTimeMock?.Dispose();
EnvironmentMock?.Dispose();
}
}
[CollectionDefinition("Shared Mock Collection")]
public class SharedMockCollection : ICollectionFixture<SharedMockFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
[Collection("Shared Mock Collection")]
public class FirstTestClass
{
private readonly SharedMockFixture _fixture;
public FirstTestClass(SharedMockFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void Test_Using_Shared_Mocks()
{
// Shared mocks are available through the fixture
var currentTime = DateTime.UtcNow;
var userName = Environment.UserName;
Assert.Equal(new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), currentTime);
Assert.Equal("TEST_USER", userName);
}
}
[Collection("Shared Mock Collection")]
public class SecondTestClass
{
private readonly SharedMockFixture _fixture;
public SecondTestClass(SharedMockFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void Another_Test_Using_Shared_Mocks()
{
var userName = Environment.UserName;
Assert.Equal("TEST_USER", userName);
}
}
xUnit Async Testing
public class AsyncXUnitTests
{
[Fact]
public async Task Async_SMock_Test()
{
using var mock = Mock.Setup(context => HttpClient.GetStringAsync(context.It.IsAny<string>()))
.Returns(Task.FromResult("{\"status\": \"success\"}"));
var httpService = new HttpService();
var result = await httpService.FetchDataAsync("https://api.example.com/data");
Assert.Contains("success", result);
}
[Theory]
[InlineData("endpoint1", "data1")]
[InlineData("endpoint2", "data2")]
public async Task Async_Theory_Test(string endpoint, string expectedData)
{
using var mock = Mock.Setup(() => ApiClient.GetAsync(endpoint))
.Returns(Task.FromResult(new ApiResponse { Data = expectedData }));
var service = new ApiService();
var result = await service.GetDataAsync(endpoint);
Assert.Equal(expectedData, result.Data);
}
}
MSTest Integration
Basic MSTest Setup
using Microsoft.VisualStudio.TestTools.UnitTesting;
using StaticMock;
[TestClass]
public class MSTestSMockTests
{
[TestInitialize]
public void Initialize()
{
// Optional test initialization
Console.WriteLine("MSTest starting with SMock");
}
[TestCleanup]
public void Cleanup()
{
// Optional test cleanup
Console.WriteLine("MSTest completed");
}
[TestMethod]
public void Basic_SMock_Test()
{
using var mock = Mock.Setup(() => DateTime.Now)
.Returns(new DateTime(2024, 1, 1));
var result = DateTime.Now;
Assert.AreEqual(new DateTime(2024, 1, 1), result);
}
}
MSTest Data-Driven Tests
[TestClass]
public class DataDrivenMSTests
{
[TestMethod]
[DataRow("test1", "result1")]
[DataRow("test2", "result2")]
[DataRow("test3", "result3")]
public void DataRow_SMock_Test(string input, string expectedOutput)
{
using var mock = Mock.Setup(() => DataProcessor.Process(input))
.Returns(expectedOutput);
var result = DataProcessor.Process(input);
Assert.AreEqual(expectedOutput, result);
}
[TestMethod]
[DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)]
public void DynamicData_SMock_Test(string input, string expectedOutput)
{
using var mock = Mock.Setup(() => DataProcessor.Process(input))
.Returns(expectedOutput);
var result = DataProcessor.Process(input);
Assert.AreEqual(expectedOutput, result);
}
public static IEnumerable<object[]> GetTestData()
{
return new[]
{
new object[] { "dynamic1", "result1" },
new object[] { "dynamic2", "result2" },
new object[] { "dynamic3", "result3" }
};
}
}
MSTest Class Initialize/Cleanup
[TestClass]
public class ClassLevelMSTests
{
private static IDisposable _classLevelMock;
[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
// Setup mocks for the entire test class
_classLevelMock = Mock.Setup(() => ConfigurationManager.AppSettings["TestMode"])
.Returns("true");
Console.WriteLine("Class-level mock initialized");
}
[ClassCleanup]
public static void ClassCleanup()
{
// Clean up class-level mocks
_classLevelMock?.Dispose();
Console.WriteLine("Class-level mock disposed");
}
[TestMethod]
public void Test_With_Class_Mock_1()
{
var testMode = ConfigurationManager.AppSettings["TestMode"];
Assert.AreEqual("true", testMode);
// Add method-specific mocks
using var dateMock = Mock.Setup(() => DateTime.Now)
.Returns(new DateTime(2024, 1, 1));
var service = new ConfigurableService();
var result = service.GetConfiguredValue();
Assert.IsNotNull(result);
}
[TestMethod]
public void Test_With_Class_Mock_2()
{
var testMode = ConfigurationManager.AppSettings["TestMode"];
Assert.AreEqual("true", testMode);
}
}
SpecFlow Integration
SpecFlow Step Definitions with SMock
// Feature file (Example.feature)
/*
Feature: File Processing with SMock
In order to test file processing
As a developer
I want to mock file system calls
Scenario: Process existing file
Given a file "test.txt" exists with content "Hello World"
When I process the file "test.txt"
Then the result should be "HELLO WORLD"
Scenario Outline: Process multiple files
Given a file "<filename>" exists with content "<content>"
When I process the file "<filename>"
Then the result should be "<expected>"
Examples:
| filename | content | expected |
| file1.txt| hello | HELLO |
| file2.txt| world | WORLD |
*/
[Binding]
public class FileProcessingSteps
{
private readonly Dictionary<string, IDisposable> _activeMocks = new();
private string _result;
[Given(@"a file ""([^""]*)"" exists with content ""([^""]*)""")]
public void GivenAFileExistsWithContent(string filename, string content)
{
// Setup file existence mock
var existsMock = Mock.Setup(() => File.Exists(filename))
.Returns(true);
_activeMocks[$"exists_{filename}"] = existsMock;
// Setup file read mock
var readMock = Mock.Setup(() => File.ReadAllText(filename))
.Returns(content);
_activeMocks[$"read_{filename}"] = readMock;
}
[When(@"I process the file ""([^""]*)""")]
public void WhenIProcessTheFile(string filename)
{
var processor = new FileProcessor();
_result = processor.ProcessFile(filename);
}
[Then(@"the result should be ""([^""]*)""")]
public void ThenTheResultShouldBe(string expectedResult)
{
Assert.AreEqual(expectedResult, _result);
}
[AfterScenario]
public void CleanupMocks()
{
foreach (var mock in _activeMocks.Values)
{
mock?.Dispose();
}
_activeMocks.Clear();
}
}
SpecFlow Hooks with SMock
[Binding]
public class SMockHooks
{
private static IDisposable _globalMock;
[BeforeTestRun]
public static void BeforeTestRun()
{
// Setup global mocks for the entire test run
_globalMock = Mock.Setup(() => Environment.GetEnvironmentVariable("TEST_ENVIRONMENT"))
.Returns("SpecFlow");
}
[AfterTestRun]
public static void AfterTestRun()
{
// Cleanup global mocks
_globalMock?.Dispose();
}
[BeforeFeature]
public static void BeforeFeature(FeatureContext featureContext)
{
Console.WriteLine($"Starting feature: {featureContext.FeatureInfo.Title} with SMock support");
}
[BeforeScenario]
public void BeforeScenario(ScenarioContext scenarioContext)
{
Console.WriteLine($"Starting scenario: {scenarioContext.ScenarioInfo.Title}");
}
[AfterScenario]
public void AfterScenario(ScenarioContext scenarioContext)
{
Console.WriteLine($"Completed scenario: {scenarioContext.ScenarioInfo.Title}");
}
}
Custom Test Frameworks
Generic Integration Pattern
For custom or less common test frameworks, follow this general pattern:
public abstract class SMockTestBase
{
private readonly List<IDisposable> _testMocks = new();
protected IDisposable CreateMock<T>(Expression<Func<T>> expression, T returnValue)
{
var mock = Mock.Setup(expression).Returns(returnValue);
_testMocks.Add(mock);
return mock;
}
protected IDisposable CreateMock<T>(Expression<Action> expression)
{
var mock = Mock.Setup(expression);
_testMocks.Add(mock);
return mock;
}
// Call this in your test framework's cleanup method
protected virtual void CleanupMocks()
{
_testMocks.ForEach(mock => mock?.Dispose());
_testMocks.Clear();
}
// Call this in your test framework's setup method
protected virtual void InitializeTest()
{
Console.WriteLine("SMock test initialized");
}
}
// Example usage with a custom framework
public class CustomFrameworkTest : SMockTestBase
{
[CustomTestMethod] // Your framework's test attribute
public void MyCustomTest()
{
// Initialize if needed
InitializeTest();
try
{
// Create mocks using the helper methods
CreateMock(() => DateTime.Now, new DateTime(2024, 1, 1));
CreateMock(context => Console.WriteLine(context.It.IsAny<string>()));
// Your test logic here
var result = DateTime.Now;
Assert.Equal(new DateTime(2024, 1, 1), result);
}
finally
{
// Ensure cleanup
CleanupMocks();
}
}
}
Framework-Agnostic Mock Manager
public class FrameworkAgnosticMockManager : IDisposable
{
private readonly List<IDisposable> _mocks = new();
private readonly Dictionary<string, object> _mockResults = new();
public IDisposable SetupMock<T>(Expression<Func<T>> expression, T returnValue, string key = null)
{
var mock = Mock.Setup(expression).Returns(returnValue);
_mocks.Add(mock);
if (key != null)
{
_mockResults[key] = returnValue;
}
return mock;
}
public IDisposable SetupMockWithCallback<T>(Expression<Func<T>> expression, T returnValue, Action<T> callback)
{
var mock = Mock.Setup(expression)
.Callback(() => callback(returnValue))
.Returns(returnValue);
_mocks.Add(mock);
return mock;
}
public T GetMockResult<T>(string key)
{
return _mockResults.ContainsKey(key) ? (T)_mockResults[key] : default(T);
}
public void Dispose()
{
_mocks.ForEach(mock => mock?.Dispose());
_mocks.Clear();
_mockResults.Clear();
}
}
// Usage example
public class AnyFrameworkTest
{
private FrameworkAgnosticMockManager _mockManager;
// Call in your framework's setup method
public void Setup()
{
_mockManager = new FrameworkAgnosticMockManager();
}
// Call in your framework's teardown method
public void Teardown()
{
_mockManager?.Dispose();
}
// Your test method
public void TestMethod()
{
_mockManager.SetupMock(() => DateTime.Now, new DateTime(2024, 1, 1), "test_date");
var result = DateTime.Now;
var expectedDate = _mockManager.GetMockResult<DateTime>("test_date");
// Use your framework's assertion method
AssertEqual(expectedDate, result);
}
private void AssertEqual<T>(T expected, T actual)
{
// Your framework's assertion implementation
if (!EqualityComparer<T>.Default.Equals(expected, actual))
{
throw new Exception($"Expected {expected}, but got {actual}");
}
}
}
CI/CD Integration
Azure DevOps Pipeline
# azure-pipelines.yml
trigger:
- master
- develop
pool:
vmImage: 'windows-latest'
variables:
buildConfiguration: 'Release'
solution: '**/*.sln'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: 'Any CPU'
configuration: '$(buildConfiguration)'
- task: VSTest@2
inputs:
platform: 'Any CPU'
configuration: '$(buildConfiguration)'
testSelector: 'testAssemblies'
testAssemblyVer2: |
**\*Tests.dll
!**\*TestAdapter.dll
!**\obj\**
searchFolder: '$(System.DefaultWorkingDirectory)'
runInParallel: true
codeCoverageEnabled: true
testRunTitle: 'SMock Integration Tests'
displayName: 'Run SMock Tests'
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testRunner: VSTest
testResultsFiles: '**/*.trx'
buildPlatform: 'Any CPU'
buildConfiguration: '$(buildConfiguration)'
GitHub Actions
# .github/workflows/test.yml
name: SMock Tests
on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
dotnet-version: ['6.0.x', '7.0.x', '8.0.x']
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ matrix.dotnet-version }}
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage" --logger trx --results-directory ./TestResults
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results-${{ matrix.os }}-${{ matrix.dotnet-version }}
path: ./TestResults
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
files: ./TestResults/**/coverage.cobertura.xml
fail_ci_if_error: true
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Restore') {
steps {
bat 'dotnet restore'
}
}
stage('Build') {
steps {
bat 'dotnet build --configuration Release --no-restore'
}
}
stage('Test') {
steps {
bat '''
dotnet test --configuration Release --no-build --verbosity normal ^
--collect:"XPlat Code Coverage" ^
--logger "trx;LogFileName=TestResults.trx" ^
--results-directory ./TestResults
'''
}
post {
always {
// Publish test results
publishTestResults testResultsPattern: 'TestResults/**/*.trx'
// Publish coverage report
publishCoverage adapters: [
istanbulCoberturaAdapter('TestResults/**/coverage.cobertura.xml')
], sourceFileResolver: sourceFiles('STORE_LAST_BUILD')
}
}
}
}
post {
always {
cleanWs()
}
}
}
Docker Integration
# Test.Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app
# Copy project files
COPY *.sln ./
COPY src/ src/
COPY tests/ tests/
# Restore dependencies
RUN dotnet restore
# Build
RUN dotnet build --configuration Release --no-restore
# Run tests
RUN dotnet test --configuration Release --no-build --verbosity normal \
--collect:"XPlat Code Coverage" \
--logger "trx;LogFileName=TestResults.trx" \
--results-directory ./TestResults
# Create test results image
FROM scratch AS test-results
COPY --from=build /app/TestResults ./TestResults
Best Practices Summary
Framework-Specific Recommendations
- NUnit: Use
[OneTimeSetUp]for expensive mock setup, leverage parallel execution - xUnit: Use collection fixtures for shared mocks, prefer constructor injection
- MSTest: Use
[ClassInitialize]for class-level mocks, leverage data-driven tests - SpecFlow: Use hooks for mock lifecycle management, keep step definitions clean
General Integration Guidelines
- Isolation: Ensure each test has proper mock cleanup
- Performance: Reuse expensive mock setups when appropriate
- Debugging: Add diagnostic logging for complex mock scenarios
- CI/CD: Include performance regression tests in your pipeline
- Documentation: Document mock setup patterns for your team
This integration guide should help you effectively use SMock with any .NET testing framework while following best practices for maintainable and reliable tests.