Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
# while mounting and running tests in Linux
*.sh text eol=lf
build text eol=lf

# Snapshot files are regenerated by AcceptSnapshotChanges; collapse them in GitHub diffs
Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/** linguist-generated=true
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Mockolate is a modern, strongly-typed mocking library for .NET, powered by sourc
- Source generator-based (no runtime proxy generation)
- Strongly-typed with compile-time safety
- AOT compatible (NativeAOT and trimming)
- Supports .NET Standard 2.0, .NET 8, .NET 10, and .NET Framework 4.8
- Supports .NET Standard 2.0, .NET 8, .NET 10, .NET 11, and .NET Framework 4.8

## Architecture

Expand Down
36 changes: 35 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run unit tests (windows)
if: matrix.os == 'windows-latest'
run: ./build.ps1 CodeCoverage
Expand Down Expand Up @@ -54,6 +55,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: API checks
run: ./build.sh ApiChecks
- name: Upload artifacts
Expand All @@ -65,6 +67,33 @@ jobs:
./Artifacts/*
./TestResults/*.trx

union-tests:
name: "Union tests (net11.0, C# preview)"
runs-on: ubuntu-latest
env:
DOTNET_NOLOGO: true
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup .NET SDKs
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Union tests
run: ./build.sh UnionTests
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: Union-tests
path: |
./Artifacts/*
./TestResults/*.trx

benchmarks:
name: "Benchmarks (${{ matrix.benchmark }})"
strategy:
Expand Down Expand Up @@ -92,6 +121,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run benchmarks
run: ./build.sh Benchmarks --benchmark-filter "*${{ matrix.benchmark }}*"
- name: Upload artifacts
Expand Down Expand Up @@ -122,6 +152,7 @@ jobs:
with:
dotnet-version: |
10.0.x
global-json-file: global.json
- name: Publish benchmark report
run: ./build.sh PublishBenchmarkReport
env:
Expand All @@ -144,6 +175,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run mutation tests
run: ./build.sh MutationTests MutationTestDashboard

Expand All @@ -164,12 +196,13 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run sonarcloud analysis
run: ./build.sh CodeAnalysis

publish-test-results:
name: "Publish Tests Results"
needs: [ api-tests, unit-tests ]
needs: [ api-tests, unit-tests, union-tests ]
runs-on: ubuntu-latest
permissions:
checks: write
Expand Down Expand Up @@ -201,6 +234,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Pack nuget packages
run: ./build.sh Pack
- name: Upload packages
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
with:
dotnet-version: |
10.0.x
global-json-file: global.json
- name: Create mutation test comment
run: ./build.sh MutationTestComment
env:
Expand All @@ -43,6 +44,7 @@ jobs:
with:
dotnet-version: |
10.0.x
global-json-file: global.json
- name: Create benchmark comment
run: ./build.sh BenchmarkComment
env:
Expand Down
34 changes: 33 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run unit tests (windows)
if: matrix.os == 'windows-latest'
run: ./build.ps1 CodeCoverage
Expand Down Expand Up @@ -53,6 +54,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: API checks
run: ./build.sh ApiChecks
- name: Upload artifacts
Expand All @@ -79,6 +81,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run mutation tests
run: ./build.sh MutationTests
- name: Upload artifacts
Expand All @@ -89,6 +92,33 @@ jobs:
path: |
./Artifacts/*

union-tests:
name: "Union tests (net11.0, C# preview)"
runs-on: ubuntu-latest
env:
DOTNET_NOLOGO: true
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup .NET SDKs
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Union tests
run: ./build.sh UnionTests
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: Union-tests
path: |
./Artifacts/*
./TestResults/*.trx

benchmarks:
name: "Benchmarks (${{ matrix.benchmark }})"
strategy:
Expand All @@ -114,6 +144,7 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run benchmarks
run: ./build.sh Benchmarks --benchmark-filter "*${{ matrix.benchmark }}*"
- name: Upload artifacts
Expand Down Expand Up @@ -143,12 +174,13 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
global-json-file: global.json
- name: Run sonarcloud analysis
run: ./build.sh CodeAnalysis

publish-test-results:
name: "Publish Tests Results"
needs: [ api-tests, unit-tests ]
needs: [ api-tests, unit-tests, union-tests ]
runs-on: ubuntu-latest
permissions:
checks: write
Expand Down
1 change: 1 addition & 0 deletions .nuke/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"Pack",
"PublishBenchmarkReport",
"Restore",
"UnionTests",
"UnitTests",
"UpdateReadme"
]
Expand Down
6 changes: 5 additions & 1 deletion Benchmarks/Mockolate.Benchmarks/Mockolate.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<!-- Pin the classic overloads so results stay comparable to the CI baseline once the SDK defaults to C# 15. -->
<Import Project="..\..\Source\Mockolate\build\Mockolate.props"/>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net11.0</TargetFramework>
<MockolateUnionParameters>false</MockolateUnionParameters>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateProgramFile>false</GenerateProgramFile>
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

Mockolate is a strongly-typed .NET mocking library powered by **Roslyn source generators**. Unlike reflection-based mocking libraries, it generates mock implementations at compile time, providing full IntelliSense support and AOT compatibility.

**Supported targets**: .NET Standard 2.0, .NET 8, .NET 10, .NET Framework 4.8
**Supported targets**: .NET Standard 2.0, .NET 8, .NET 10, .NET 11, .NET Framework 4.8

## Build and Test Commands

Expand All @@ -33,7 +33,7 @@ dotnet test Tests/Mockolate.Tests/Mockolate.Tests.csproj --filter "FullyQualifie
./build.sh Pack # Create NuGet packages
```

**Requires**: .NET 10 SDK (see `global.json`). For source generator changes, run `dotnet clean && dotnet build` to force regeneration.
**Requires**: .NET 11 SDK (currently a preview, see `global.json`). For source generator changes, run `dotnet clean && dotnet build` to force regeneration.

## Architecture

Expand All @@ -53,6 +53,7 @@ An `IIncrementalGenerator` that runs at compile time and generates `Mock.{TypeNa
- Handles interfaces, abstract classes, and delegates
- Emits method overrides that delegate to `MockRegistry`
- Targets .NET Standard 2.0 (Roslyn constraint)
- Union mode (`Sources.MockClass.Unions.cs`): with C# 15 (or `MockolateUnionParameters=true`) the setup/verify overloads take `ParameterArg<T>?` or `Func<T, bool>` per parameter instead of the classic matcher/value set; below that the output is unchanged

### Source/Mockolate.Analyzers + Source/Mockolate.Analyzers.CodeFixers
Roslyn analyzers that validate mock usage at compile time:
Expand Down
2 changes: 1 addition & 1 deletion Docs/pages/00-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Nuget](https://img.shields.io/nuget/v/Mockolate)](https://www.nuget.org/packages/Mockolate)

[**Mockolate**](https://github.com/Testably/Mockolate) is a modern, strongly-typed, AOT-compatible mocking library for .NET, powered by source generators.
It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, .NET 10 and .NET Framework 4.8.
It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, .NET 10, .NET 11 and .NET Framework 4.8.

- **Source generator-based**: No runtime proxy generation.
- **Fast**: Direct dispatch with no reflection or dynamic proxies.
Expand Down
3 changes: 2 additions & 1 deletion Docs/pages/setup/02-methods.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Methods

Use `sut.Mock.Setup.MethodName(…)` to set up methods. You can specify argument matchers for each parameter.
Use `sut.Mock.Setup.MethodName(…)` to set up methods. You can specify argument matchers for each parameter; with C# 15
you can also pass predicates directly (see *Union Parameters* under Parameter Matching).

## Returns / Throws

Expand Down
13 changes: 13 additions & 0 deletions Docs/pages/setup/03-indexers.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,16 @@ does not intercept.
methods.
- Use `.SkippingBaseClass(…)` to override the base class behavior for a specific indexer (only for class mocks).
- When you specify overlapping setups, the most recently defined setup takes precedence.

## Union Parameters (C# 15)

With C# 15, indexer keys accept predicates directly, in setups and verifications alike:

```csharp
sut.Mock.Setup[type => type.StartsWith("D")].Returns(10);

sut.Mock.Verify[type => type.StartsWith("D")].Got().Once();
```

Only an indexer that is the sole indexer of its key count on the type takes union-typed keys; indexers sharing a key
count keep the classic matcher/value overloads. See *Union Parameters* under Parameter Matching for the details.
38 changes: 38 additions & 0 deletions Docs/pages/setup/04-parameter-matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,44 @@ The following cases are rejected at compile time with diagnostic `Mockolate0003`
- `out` / `ref` / `ref readonly` parameters of a ref-struct type.
- Methods that return a custom ref struct. (`Span<T>` / `ReadOnlySpan<T>` returns are supported.)

## Union Parameters (C# 15)

When the consuming project compiles with C# 15, Mockolate generates the setup and verify overloads with union-typed
parameters: every value-capable parameter becomes a `ParameterArg<T>?` that accepts an `It` matcher or a direct value,
and a `Func<T, bool>` overload accepts a predicate. The number of overloads stays the same as before, but predicates no
longer need `It.Satisfies`:

```csharp
sut.Mock.Setup.Dispense(type => type.StartsWith("D"), It.IsAny<int>()).Returns(true);

sut.Mock.Verify.Dispense("Dark", amount => amount > 10).Once();
```

Union mode is enabled automatically once the compiler ships C# 15 and the project's effective language version is
C# 15 or later. On a preview compiler, opt in with `<MockolateUnionParameters>true</MockolateUnionParameters>` in the
project file; `false` keeps the classic overloads on any compiler.

Behaviour of the union-typed parameters:

- `null` and `default` stand for the parameter's declared default value, or for `default(T)` when it has none, exactly
as with the classic value overloads.
- A parameter whose type is a delegate offers the raw delegate instead of a predicate, so a lambda is still matched as
a value.
- For an `object` parameter, a lambda literal is a predicate, while a delegate stored in a variable is a value.
- Within a union-typed member, `ref`, `out` and `ref readonly` parameters as well as `Span<T>` and `ReadOnlySpan<T>`
parameters keep their matcher slot (no predicate for that parameter); `in` parameters behave like by-value ones.
- Overloaded method names (including a generic sibling; a same-named member in another scope — public, protected or
static — does not count), generic methods, `params` methods, members with custom ref-struct parameters, and indexers
that share their key count with another same-scope indexer keep the classic matcher/value overloads. Use
`It.Satisfies` for predicates there.

The generated `ParameterArg<T>` type relies on `UnionAttribute`, `OverloadResolutionPriorityAttribute` and
`CallerArgumentExpressionAttribute`. Mockolate declares whichever of them the compilation lacks (neither the referenced
frameworks nor the project itself provide it). Another generator such as PolySharp can provide them without Mockolate
noticing; set `<MockolateUnionAttributePolyfills>false</MockolateUnionAttributePolyfills>` to declare that
`OverloadResolutionPriorityAttribute` and `CallerArgumentExpressionAttribute` come from elsewhere (`UnionAttribute`
keeps following the compilation), or list the attribute names the project provides.

## Parameter Predicates

When the method name is unique (no overloads), you can use argument matchers from the `Match` class for more flexible parameters matching:
Expand Down
32 changes: 32 additions & 0 deletions Pipeline/Build.UnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,36 @@ partial class Build
), completeOnFailure: true
);
});

Project[] UnionTestProjects =>
[
Solution.Tests.Mockolate_Tests,
Solution.Tests.Mockolate_Internal_Tests,
Solution.Tests.Mockolate_SourceGenerators_Tests,
Solution.Tests.Mockolate_ExampleTests,
];

/// <summary>
/// Runs the test projects on net11.0 only, where <c>Tests/Directory.Build.props</c> enables the C# preview
/// language version and the union-typed setup surface (<c>MockolateUnionParameters</c>).
/// </summary>
Target UnionTests => _ => _
.Unlisted()
.DependsOn(Compile)
.Executes(() =>
{
DotNetTest(s => s
.SetConfiguration(Configuration)
.SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US")
.EnableNoBuild()
.SetFramework("net11.0")
.SetResultsDirectory(TestResultsDirectory)
.CombineWith(
UnionTestProjects,
(settings, project) => settings
.SetProjectFile(project)
.AddLoggers($"trx;LogFileName={project.Name}_net11.0_unions.trx")
), completeOnFailure: true
);
});
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FTestably%2FMockolate%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/Testably/Mockolate/main)

**Mockolate** is a modern, strongly-typed, AOT-compatible mocking library for .NET, powered by source generators.
It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, .NET 10 and .NET Framework 4.8.
It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, .NET 10, .NET 11 and .NET Framework 4.8.

- **Source generator-based**: No runtime proxy generation.
- **Fast**: Direct dispatch with no reflection or dynamic proxies.
Expand Down
2 changes: 1 addition & 1 deletion Source/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</PropertyGroup>

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net10.0;net11.0</TargetFrameworks>
</PropertyGroup>

<PropertyGroup>
Expand Down
Loading
Loading