Jitest 0.9.0

dotnet add package Jitest --version 0.9.0
                    
NuGet\Install-Package Jitest -Version 0.9.0
                    
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="Jitest" Version="0.9.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Jitest" Version="0.9.0" />
                    
Directory.Packages.props
<PackageReference Include="Jitest" />
                    
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 Jitest --version 0.9.0
                    
#r "nuget: Jitest, 0.9.0"
                    
#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 Jitest@0.9.0
                    
#: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=Jitest&version=0.9.0
                    
Install as a Cake Addin
#tool nuget:?package=Jitest&version=0.9.0
                    
Install as a Cake Tool

<div align="center">

Jitest

JIT-time Method Interceptor for Unit Testing

</div>

Jitest is an experimental C# library for replacing method behavior entirely from test code. It keeps test-only abstractions such as interfaces, virtual members, and others out of production design.

typeof(Stopwatch)
    .Jitest<Func<long, TimeSpan>>("GetElapsedTime", out var original)
    .Intercept(static (long _) => new TimeSpan(9, 8, 7, 6, 5, 4));

Stopwatch.GetElapsedTime(any_value);  // Returns 9 days 08:07:06 + 5 ms + 4 us

Motivation

Production code should not need to know how it will be tested. Jitest lets a test depend explicitly on implementation details and replace behavior only for the lifetime of that test scope.

This avoids changing production code solely to make a test possible, for example by:

  • Adding an interface
  • Making a member virtual
  • Removing sealed from a class
  • Passing a time or environment abstraction through the application

Getting Started

Supported frameworks:

  • .NET 5 or later
  • .NET Standard 2.1

API Overview

  • typeof(Target).Jitest<TDelegate>(...) intercepts a static method.
  • instance.Jitest<TDelegate>(...) intercepts an instance method.
  • Methods are resolved by exact name and signature across the type hierarchy, regardless of visibility.
  • TDelegate describes the explicit parameters and return type of the target method.
    • A replacement for an instance method adds a compatible declaring-type parameter at the beginning.
  • The out argument of Intercept receives a clone of the original method that bypasses the hook.
  • Use with using or dispose DetourScope returned by Intercept to remove the hook.

Test Setup Requirements

Jitest deliberately moves the testing dependency into the test instead of changing production architecture for testability. Use it only where the runtime-wide impact of the selected method can be controlled.

  • Static BCL methods have an unpredictable impact radius and are generally unsuitable targets. Prefer a method owned by the tested code whose callers are known, such as a private IsTimedOut instance method.
  • Instance methods on value types are not supported.
  • Constructors, abstract methods, and open generic methods are not supported.
  • Target and replacement parameter and return types must match (TODO: strongly-typed interceptor).

Global Test Initialization

Install all required interceptions once during the test framework's global initialization. Create a dedicated instance for each fixed behavior, such as AlwaysNotFoundHttpClient and AlwaysTimeoutHttpClient, and keep the interceptions unchanged until global cleanup.

Tests can then run concurrently because they only select a preconfigured instance and never replace method behavior while tests are running. Create every required instance and interception scope before parallel test execution starts, and dispose them only after all tests have finished.

Serializing Test Methods

Alternatively, acquire a JitestContext scope once at the beginning of each participating test method. The scope holds a global semaphore until it is disposed, preventing those test methods from running concurrently even when interception begins later in the test or is not used at all.

[Fact]
public void ExampleTest()
{
    using var testMethodScope = JitestContext.BeginTestMethod();

    // Arrange, intercept if required, act, and assert.
}

Every test that can reach an intercepted method must follow this convention. Calling BeginTestMethod only from tests that use Intercept does not protect other tests from a process-wide hook.

Intercepting Methods

Passing an instance to Jitest selects the instance method to intercept. Currently, it does not target only that instance (TODO). The hook affects the same method on every instance. Use the replacement delegate's first argument to select the intended instance explicitly.

Static Methods

Within using scope, Stopwatch.GetElapsedTime(long) always returns 9 days, 8 hours, 7 minutes, 6 seconds, 5 milliseconds, and 4 microseconds.

using var _ = typeof(Stopwatch)
    .Jitest<Func<long, TimeSpan>>(nameof(Stopwatch.GetElapsedTime), out _)
    .Intercept(static (long _) => new TimeSpan(9, 8, 7, 6, 5, 4));

Intercepting static BCL methods is generally unsafe. The hook affects the entire process, and test serialization cannot isolate calls made by the runtime or unrelated code. Prefer intercepting a private or internal instance method owned by the test subject whose callers and impact can be controlled.

Instance Methods

For an instance method, the replacement delegate must explicitly include the target instance as its first argument. This is the hidden argument that C# normally inserts at the call site.

For example, HttpClient.GetAsync(string) replacement will look like:

using var _ = client.Jitest<...>(...)
    // Requires a hidden first argument of HttpClient
    .Intercept((HttpClient instance, string url) => { ... };
Restricting behavior to one instance

Use the cloned original method returned by Jitest and filter the target inside the replacement. Do not call the intercepted method directly: that would enter the hook again and cause stack overflow.

using var targetClient = new HttpClient();

using var _ = targetClient
    .Jitest<Func<string, Task<HttpResponseMessage>>>(nameof(HttpClient.GetAsync), out var originalGetAsync)
    .Intercept((HttpClient instance, string requestUri) =>
    {
        // The hook is process-wide, so explicitly forward calls for every other instance.
        return ReferenceEquals(instance, targetClient)
            ? Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotExtended)
            {
                Version = new Version(310, 42),
            })
            : originalGetAsync.Invoke(requestUri);
    });

    // BAD EXAMPLE
    // This affects every call to GetAsync in the process, not only calls made through client.
    .Intercept(static (HttpClient _, string _) =>
        Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotExtended)
        {
            Version = new Version(310, 42),
        }));
Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 is compatible.  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 is compatible.  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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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.9.0 131 9/13/2026