Table of Contents

Getting Started with SMock

Welcome to SMock, the only .NET library that makes static method mocking effortless! This comprehensive guide will walk you through everything you need to know to start using SMock in your test projects.

Table of Contents

What Makes SMock Special

The Static Method Problem

Traditional mocking frameworks like Moq, NSubstitute, and FakeItEasy can only mock virtual methods and interfaces. They cannot mock static methods, which leaves developers struggling with:

  • Legacy Code: Older codebases with heavy static method usage
  • Third-Party Dependencies: External libraries with static APIs (File.*, DateTime.Now, etc.)
  • System APIs: .NET Framework/Core static methods
  • Testing Isolation: Creating predictable test environments

The SMock Solution

SMock uses runtime IL modification via MonoMod to intercept method calls at the CLR level:

// Traditional approach - can't mock this!
var content = File.ReadAllText("config.json"); // Real file system call

// SMock approach - full control!
using var mock = Mock.Setup(() => File.ReadAllText("config.json"))
    .Returns("{\"test\": \"data\"}");

var content = File.ReadAllText("config.json"); // Returns mocked data!

Installation & Setup

NuGet Package Installation

Install SMock via your preferred method:

# Package Manager Console
Install-Package SMock

# .NET CLI
dotnet add package SMock

# PackageReference
<PackageReference Include="SMock" Version="*" />

Framework Support

SMock supports a wide range of .NET implementations:

Target Framework Support Notes
.NET 5.0+ ✅ Full Recommended
.NET Core 2.0+ ✅ Full Excellent performance
.NET Framework 4.62-4.81 ✅ Full Legacy support
.NET Standard 2.0+ ✅ Full Library compatibility

First Test Setup

No special configuration required! SMock works with any test framework:

using NUnit.Framework;
using NUnit.Framework.Legacy;
using StaticMock;

[TestFixture]
public class MyFirstTests
{
    [Test]
    public void MyFirstMockTest()
    {
        // SMock is ready to use immediately!
        using var mock = Mock.Setup(() => DateTime.Now)
            .Returns(new DateTime(2024, 1, 1));

        var testDate = DateTime.Now;
        ClassicAssert.AreEqual(new DateTime(2024, 1, 1), testDate);
    }
}

Understanding the Two API Styles

SMock provides two distinct API styles to match different testing preferences and scenarios.

Sequential API - Disposable & Clean

Best for: Straightforward mocking with automatic cleanup

Characteristics:

  • Uses using statements for automatic cleanup
  • Returns IDisposable mock objects
  • Clean, scoped mocking
  • Perfect for most testing scenarios
[Test]
public void Sequential_API_Example()
{
    // Each mock is disposable - use context parameter for parameter matching
    using var existsMock = Mock.Setup(context => File.Exists(context.It.IsAny<string>()))
        .Returns(true);

    using var readMock = Mock.Setup(context => File.ReadAllText(context.It.IsAny<string>()))
        .Returns("file content");

    // Test the mocked file operations directly
    var exists = File.Exists("test.txt");
    var content = File.ReadAllText("test.txt");

    ClassicAssert.IsTrue(exists);
    ClassicAssert.AreEqual("file content", content);
} // Mocks automatically cleaned up

Hierarchical API - Validation & Control

Best for: Complex scenarios requiring inline validation

Characteristics:

  • Includes validation actions that run during mock execution
  • No using statements needed
  • Perfect for complex assertion scenarios
  • Great for behavior verification
[Test]
public void Hierarchical_API_Example()
{
    const string expectedPath = "important.txt";
    const string mockContent = "validated content";

    Mock.Setup(context => File.ReadAllText(context.It.IsAny<string>()), () =>
    {
        // This validation runs DURING the mock call
        var content = File.ReadAllText(expectedPath);
        ClassicAssert.IsNotNull(content);
        ClassicAssert.AreEqual(mockContent, content);

        // You can even verify the mock was called with correct parameters
    }).Returns(mockContent);

    // Test your code - validation happens automatically
    var actualContent = File.ReadAllText("important.txt");
    ClassicAssert.AreEqual(mockContent, actualContent);
}

When to Use Which Style?

Scenario Recommended Style Reason
Simple return value mocking Sequential Cleaner syntax, automatic cleanup
Parameter verification Hierarchical Built-in validation actions
Multiple related mocks Sequential Better with using statements
Complex behavior testing Hierarchical Inline validation capabilities
One-off mocks Sequential Simpler dispose pattern

Your First Mocks

Mocking Static Methods

[Test]
public void Mock_DateTime_Now()
{
    var fixedDate = new DateTime(2024, 12, 25, 10, 30, 0);

    using var mock = Mock.Setup(() => DateTime.Now)
        .Returns(fixedDate);

    // Your code that uses DateTime.Now
    var currentDate = DateTime.Now;

    ClassicAssert.AreEqual(fixedDate, currentDate);
    ClassicAssert.AreEqual(2024, currentDate.Year);
    ClassicAssert.AreEqual(12, currentDate.Month);
    ClassicAssert.AreEqual(25, currentDate.Day);
    ClassicAssert.AreEqual(10, currentDate.Hour);
    ClassicAssert.AreEqual(30, currentDate.Minute);
}

Mocking Static Methods with Parameters

[Test]
public void Mock_File_Operations()
{
    using var existsMock = Mock.Setup(context => File.Exists(context.It.IsAny<string>()))
        .Returns(true);

    using var readMock = Mock.Setup(context => File.ReadAllText(context.It.IsAny<string>()))
        .Returns("{\"database\": \"localhost\", \"port\": 5432}");

    // Test file operations
    var exists = File.Exists("config.json");
    var content = File.ReadAllText("config.json");

    ClassicAssert.IsTrue(exists);
    ClassicAssert.AreEqual("{\"database\": \"localhost\", \"port\": 5432}", content);
    ClassicAssert.IsTrue(content.Contains("localhost"));
    ClassicAssert.IsTrue(content.Contains("5432"));
}

Mocking Instance Methods

Yes! SMock can also mock instance methods:

[Test]
public void Mock_Instance_Method()
{
    var testUser = new User { Name = "Test User" };

    using var mock = Mock.Setup(() => testUser.GetDisplayName())
        .Returns("Mocked Display Name");

    var result = testUser.GetDisplayName();
    ClassicAssert.AreEqual("Mocked Display Name", result);
}

Mocking Properties

[Test]
public void Mock_Static_Property()
{
    using var mock = Mock.Setup(() => Environment.MachineName)
        .Returns("TEST-MACHINE");

    var machineName = Environment.MachineName;
    ClassicAssert.AreEqual("TEST-MACHINE", machineName);
}

Parameter Matching

SMock provides powerful parameter matching through the It class:

Basic Parameter Matching

[Test]
public void Parameter_Matching_Examples()
{
    // Match any string parameter
    using var anyStringMock = Mock.Setup(context => Path.GetFileName(context.It.IsAny<string>()))
        .Returns("mocked-file.txt");

    // Test with different paths
    var result1 = Path.GetFileName(@"C:\temp\test.txt");
    var result2 = Path.GetFileName(@"D:\documents\report.docx");

    ClassicAssert.AreEqual("mocked-file.txt", result1);
    ClassicAssert.AreEqual("mocked-file.txt", result2);
}

Advanced Parameter Matching

[Test]
public void Advanced_Parameter_Matching()
{
    // Note: Conditional parameter matching with It.Is has current limitations
    // This example shows the expected syntax once fully implemented
    using var mock = Mock.Setup(context =>
        Convert.ToInt32(context.It.IsAny<string>()))
        .Returns(42);

    var result = Convert.ToInt32("123");
    ClassicAssert.AreEqual(42, result);
}

Parameter Matching with Hierarchical API

[Test]
public void Hierarchical_Parameter_Validation()
{
    Mock.Setup(context => Path.Combine(context.It.IsAny<string>(), context.It.IsAny<string>()), () =>
    {
        // Validate the actual parameters that were passed
        var result = Path.Combine("test", "path");
        ClassicAssert.IsNotNull(result);
        ClassicAssert.IsTrue(result.Contains("test"));
        ClassicAssert.IsTrue(result.Contains("path"));
    }).Returns(@"test\path");

    var combinedPath = Path.Combine("test", "path");
    ClassicAssert.AreEqual(@"test\path", combinedPath);
}

Async Support

SMock provides full support for async/await patterns:

Mocking Async Methods

[Test]
public async Task Mock_Async_Methods()
{
    // Mock async Task.FromResult
    using var mock = Mock.Setup(() => Task.FromResult(42))
        .Returns(Task.FromResult(100));

    var result = await Task.FromResult(42);
    ClassicAssert.AreEqual(100, result);
}

Mocking with Delays

[Test]
public async Task Mock_Async_With_Delay()
{
    // Mock Task.Delay to complete immediately
    using var delayMock = Mock.Setup(context => Task.Delay(context.It.IsAny<int>()))
        .Returns(Task.CompletedTask);

    // Simulate an async operation that would normally take time
    await Task.Delay(5000); // This should complete immediately

    ClassicAssert.Pass("Async mock executed successfully");
}

Exception Handling with Async

[Test]
public async Task Mock_Async_Exceptions()
{
    // Mock Task.Delay to throw an exception for negative values
    using var mock = Mock.Setup(context => Task.Delay(context.It.Is<int>(ms => ms < 0)))
        .Throws<ArgumentOutOfRangeException>();

    // Test exception handling in async context
    try
    {
        await Task.Delay(-1);
        Assert.Fail("Expected ArgumentOutOfRangeException to be thrown");
    }
    catch (ArgumentOutOfRangeException exception)
    {
        ClassicAssert.IsNotNull(exception);
    }
}

Advanced Scenarios

Callback Execution

Execute custom logic when mocks are called:

[Test]
public void Mock_With_Callbacks()
{
    var callCount = 0;

    using var mock = Mock.Setup(context => File.WriteAllText(context.It.IsAny<string>(), context.It.IsAny<string>()))
        .Callback<string, string>((path, content) => callCount++);

    File.WriteAllText("test.txt", "content");
    File.WriteAllText("test2.txt", "content2");

    ClassicAssert.AreEqual(2, callCount);
}

Sequential Return Values

Return different values on successive calls:

[Test]
public void Sequential_Return_Values()
{
    var callCount = 0;

    using var mock = Mock.Setup(() => DateTime.Now)
        .Returns(() =>
        {
            callCount++;
            return callCount switch
            {
                1 => new DateTime(2024, 1, 1),
                2 => new DateTime(2024, 1, 2),
                _ => new DateTime(2024, 1, 3)
            };
        });

    var date1 = DateTime.Now;
    var date2 = DateTime.Now;
    var date3 = DateTime.Now;

    ClassicAssert.AreEqual(new DateTime(2024, 1, 1), date1);
    ClassicAssert.AreEqual(new DateTime(2024, 1, 2), date2);
    ClassicAssert.AreEqual(new DateTime(2024, 1, 3), date3);
}

Conditional Mocking

Different behaviors based on parameters:

[Test]
public void Conditional_Mock_Behavior()
{
    using var mock = Mock.Setup(context => Environment.GetEnvironmentVariable(context.It.IsAny<string>()))
        .Returns<string>(varName => varName switch
        {
            "ENVIRONMENT" => "Development",
            "DEBUG_MODE" => "true",
            "LOG_LEVEL" => "Debug",
            _ => null
        });

    var environment = Environment.GetEnvironmentVariable("ENVIRONMENT");
    var debugMode = Environment.GetEnvironmentVariable("DEBUG_MODE");
    var logLevel = Environment.GetEnvironmentVariable("LOG_LEVEL");
    var unknown = Environment.GetEnvironmentVariable("UNKNOWN");

    ClassicAssert.AreEqual("Development", environment);
    ClassicAssert.AreEqual("true", debugMode);
    ClassicAssert.AreEqual("Debug", logLevel);
    ClassicAssert.IsNull(unknown);
}

Best Practices

1. Scope Mocks Appropriately

[Test]
public void Good_Mock_Scoping()
{
    // ✅ Good: Scope mocks to specific test needs
    using var timeMock = Mock.Setup(() => DateTime.Now)
        .Returns(new DateTime(2024, 1, 1));

    using var fileMock = Mock.Setup(() => File.Exists("config.json"))
        .Returns(true);

    // Test logic here
}

[Test]
public void Bad_Mock_Scoping()
{
    // ❌ Bad: Don't create mocks you don't use in the test
    using var unnecessaryMock = Mock.Setup(context => Console.WriteLine(context.It.IsAny<string>()));

    // Test that doesn't use Console.WriteLine
}

2. Use Meaningful Return Values

[Test]
public void Meaningful_Return_Values()
{
    // ✅ Good: Return values that make sense for the test
    using var mock = Mock.Setup(() => UserRepository.GetUserById(123))
        .Returns(new User
        {
            Id = 123,
            Name = "Test User",
            Email = "test@example.com",
            IsActive = true
        });

    // ❌ Bad: Return meaningless default values
    using var badMock = Mock.Setup(() => UserRepository.GetUserById(456))
        .Returns(new User()); // Empty object with no meaningful data
}
[Test]
public void Group_Related_Mocks()
{
    // ✅ Good: Group mocks that work together
    using var existsMock = Mock.Setup(() => File.Exists("database.config"))
        .Returns(true);
    using var readMock = Mock.Setup(() => File.ReadAllText("database.config"))
        .Returns("connection_string=test_db");
    using var writeMock = Mock.Setup(context => File.WriteAllText(context.It.IsAny<string>(), context.It.IsAny<string>()));

    // Test configuration management
    var configManager = new ConfigurationManager();
    configManager.UpdateConfiguration("new_setting", "value");
}

4. Verify Mock Usage When Needed

[Test]
public void Verify_Mock_Usage()
{
    var callCount = 0;

    using var mock = Mock.Setup(context => AuditLogger.LogAction(context.It.IsAny<string>()))
        .Callback<string>(action => callCount++);

    var service = new CriticalService();
    service.PerformCriticalOperation();

    // Verify the audit log was called
    Assert.AreEqual(1, callCount, "Audit logging should be called exactly once");
}

Common Patterns

Configuration Testing

[Test]
public void Configuration_Pattern()
{
    var testConfig = new Dictionary<string, string>
    {
        ["DatabaseConnection"] = "test_connection",
        ["ApiKey"] = "test_key_12345",
        ["EnableFeatureX"] = "true"
    };

    using var mock = Mock.Setup(context =>
        ConfigurationManager.AppSettings[context.It.IsAny<string>()])
        .Returns<string>(key => testConfig.GetValueOrDefault(key));

    var service = new ConfigurableService();
    service.Initialize();

    Assert.IsTrue(service.IsFeatureXEnabled);
    Assert.AreEqual("test_connection", service.DatabaseConnection);
}

Time-Dependent Testing

[Test]
public void Time_Dependent_Pattern()
{
    var testDate = new DateTime(2024, 6, 15, 14, 30, 0); // Saturday afternoon

    using var mock = Mock.Setup(() => DateTime.Now)
        .Returns(testDate);

    var scheduler = new TaskScheduler();
    var nextRun = scheduler.CalculateNextBusinessDay();

    // Should be Monday since Saturday -> next business day is Monday
    Assert.AreEqual(DayOfWeek.Monday, nextRun.DayOfWeek);
    Assert.AreEqual(new DateTime(2024, 6, 17), nextRun.Date);
}

External Dependency Testing

[Test]
public void External_Dependency_Pattern()
{
    // Mock external web service
    using var webMock = Mock.Setup(() =>
        WebClient.DownloadString("https://api.weather.com/current"))
        .Returns("{\"temperature\": 22, \"condition\": \"sunny\"}");

    // Mock file system for caching
    using var fileMock = Mock.Setup(context =>
        File.WriteAllText(context.It.IsAny<string>(), context.It.IsAny<string>()));

    var weatherService = new WeatherService();
    var weather = weatherService.GetCurrentWeather();

    Assert.AreEqual(22, weather.Temperature);
    Assert.AreEqual("sunny", weather.Condition);
}

Troubleshooting

Common Issues and Solutions

Issue: Mock Not Triggering

Problem: Your mock setup looks correct, but the original method is still being called.

// ❌ This might not work as expected
Mock.Setup(() => SomeClass.Method()).Returns("mocked");
var result = SomeClass.Method(); // Still calls original!

Solution: Ensure you're calling the exact same method signature.

// ✅ Make sure parameter types match exactly
Mock.Setup(context => SomeClass.Method(context.It.IsAny<string>())).Returns("mocked");
var result = SomeClass.Method("any_parameter"); // Now mocked!

Issue: Parameter Matching Not Working

Problem: Your parameter matcher seems too restrictive.

// ❌ Too specific
Mock.Setup(() => Validator.Validate("exact_string")).Returns(true);
var result = Validator.Validate("different_string"); // Not mocked!

Solution: Use appropriate parameter matchers.

// ✅ Use IsAny for flexible matching
Mock.Setup(context => Validator.Validate(context.It.IsAny<string>())).Returns(true);

// ✅ Or use Is with conditions
Mock.Setup(context => Validator.Validate(context.It.Is<string>(s => s.Length > 0))).Returns(true);

Issue: Async Mocks Not Working

Problem: Async methods aren't being mocked properly.

// ❌ Wrong return type
Mock.Setup(() => Service.GetDataAsync()).Returns("data"); // Won't compile!

Solution: Return the correct Task type.

// ✅ Return Task with proper value
Mock.Setup(() => Service.GetDataAsync()).Returns(Task.FromResult("data"));

// ✅ Or use async lambda
Mock.Setup(() => Service.GetDataAsync()).Returns(async () =>
{
    await Task.Delay(10);
    return "data";
});

Issue: Mocks Interfering Between Tests

Problem: Mocks from one test affecting another.

Solution: Always use using statements with Sequential API to ensure proper cleanup.

[Test]
public void Test_With_Proper_Cleanup()
{
    using var mock = Mock.Setup(() => DateTime.Now)
        .Returns(new DateTime(2024, 1, 1));

    // Test logic here
} // Mock automatically disposed and cleaned up

Getting Help

When you encounter issues:

  1. Check the Documentation: Review this guide and the API reference
  2. Search Issues: Check GitHub Issues for similar problems
  3. Create Minimal Repro: Prepare a minimal code example that demonstrates the issue
  4. Ask for Help: Create a new issue with details about your environment and problem

Performance Considerations

  • Mock Setup Cost: Creating mocks has a small one-time cost (~1-2ms per mock)
  • Runtime Overhead: Method interception is very fast (<0.1ms per call)
  • Memory Usage: Minimal impact, temporary IL modifications only
  • Cleanup: Always dispose Sequential mocks to free resources promptly

Next Steps

Now that you understand the basics of SMock, continue your journey with these comprehensive guides:

🚀 Level Up Your Skills

🛠️ Optimization & Troubleshooting

📚 Reference Materials

💡 Quick Navigation by Use Case

Happy testing with SMock! 🚀

Working Examples in the Test Suite

All examples in this documentation are based on actual working test cases. You can find complete, debugged examples in the SMock test suite:

📁 Basic Examples

📁 Advanced Examples

📁 Migration & Integration

💡 Why Reference the Test Examples?

The test examples provide:

  • Verified working code - All examples compile and pass tests
  • Complete context - Full test methods with setup and teardown
  • Current limitations - Some examples include [Ignore] attributes with notes about current implementation constraints
  • Best practices - Real-world usage patterns and error handling
  • Latest syntax - Up-to-date API usage that matches the current implementation

🔧 Running the Examples Locally

To run these examples on your machine:

# Clone the repository
git clone https://github.com/SvetlovA/static-mock.git
cd static-mock/src

# Run the specific example tests
dotnet test --filter "FullyQualifiedName~Examples"

# Or run a specific example class
dotnet test --filter "ClassName=BasicSequentialExamples"