Summary
With the experimental compile-time internals access (TUnitMocksExperimentalInternalsAccess + TUnitMocksInternalsAccess, #6514 Tier 2 / #6520), dotnet build succeeds and the test passes, but every IDE / Roslyn-workspace-based tool reports a false CS0122 'X' is inaccessible due to its protection level on the internal type.
Cause: TUnitMocksPublicizeReferences swaps the publicized copy into ReferencePathWithRefAssemblies only. Design-time builds and MSBuildWorkspace resolve references from ReferencePath, which still points at the original, un-publicized assembly.
Repro
SdkLib/Api.cs:
namespace SdkLib;
internal interface IQuotaPolicy
{
bool Allow(string clientId);
}
Tests/Tests.csproj:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TUnitMocksExperimentalInternalsAccess>true</TUnitMocksExperimentalInternalsAccess>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" Version="1.68.0" />
<PackageReference Include="TUnit.Mocks" Version="1.68.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SdkLib\SdkLib.csproj" />
<TUnitMocksInternalsAccess Include="SdkLib" />
</ItemGroup>
Tests/QuotaTests.cs:
using SdkLib;
using TUnit.Mocks;
public class QuotaTests
{
[Test]
public async Task Internal_policy_is_mockable()
{
var policy = IQuotaPolicy.Mock();
policy.Allow(Any()).Returns(false);
await Assert.That(policy.Object.Allow("acme")).IsFalse();
}
}
Actual vs expected
dotnet build → 0 warnings, 0 errors. dotnet run → 1/1 passed. Correct.
Loading the same project through Roslyn's MSBuildWorkspace (what OmniSharp and the C# language server use) and asking the compilation for diagnostics:
WORKSPACE-ERROR CS0122 10,21: 'IQuotaPolicy' is inaccessible due to its protection level
error count: 1
In an editor the whole test is underlined red, and a second error appears where the mock is passed on:
CS1503: Argument 1: cannot convert from 'SdkLib.IQuotaPolicy?' to 'SdkLib.IQuotaPolicy?'
Expected: the IDE agrees with the compiler, since the build is authoritative and correct.
Root cause
$ dotnet msbuild Tests/Tests.csproj -t:Compile -p:SkipCompilerExecution=true -p:ProvideCommandLineArgs=true -getItem:ReferencePath
"Identity": "/tmp/iavrepro/SdkLib/bin/Debug/net10.0/SdkLib.dll" # original
$ ... -getItem:ReferencePathWithRefAssemblies
"Identity": "obj/Debug/net10.0/tunit-mocks-internals/68fe7f1d/SdkLib.dll" # publicized
$ dotnet msbuild Tests/Tests.csproj -t:ResolveAssemblyReferencesDesignTime -p:DesignTimeBuild=true -getItem:ReferencePath
"Identity": "/tmp/iavrepro/SdkLib/bin/Debug/net10.0/SdkLib.dll" # original
TUnit.Mocks.InternalsAccess.targets does this deliberately, and the comment explains why — ReferencePath has to keep the original so copy-local and deps.json bind to the real assembly at runtime. That reasoning holds for a real build, but it also means no design-time consumer ever sees the publicized copy.
Suggested fix
Swap ReferencePath as well when '$(DesignTimeBuild)' == 'true'. Nothing is copied in a design-time build, so the copy-local / deps.json argument does not apply there, and the editor would then see what the compiler sees.
If that is not workable, it would be worth a line in the docs for the feature, since the failure mode looks exactly like a broken setup.
Versions
TUnit / TUnit.Mocks 1.68.0, net10.0, SDK 10.0.401, macOS arm64
- Same behaviour path in 1.63.0–1.68.0:
TUnit.Mocks.InternalsAccess.targets is unchanged across those releases
- Probe used:
Microsoft.CodeAnalysis.Workspaces.MSBuild 4.14.0 + Microsoft.Build.Locator 1.7.8, opening Tests.csproj and printing Compilation.GetDiagnostics() errors
Summary
With the experimental compile-time internals access (
TUnitMocksExperimentalInternalsAccess+TUnitMocksInternalsAccess, #6514 Tier 2 / #6520),dotnet buildsucceeds and the test passes, but every IDE / Roslyn-workspace-based tool reports a falseCS0122 'X' is inaccessible due to its protection levelon the internal type.Cause:
TUnitMocksPublicizeReferencesswaps the publicized copy intoReferencePathWithRefAssembliesonly. Design-time builds andMSBuildWorkspaceresolve references fromReferencePath, which still points at the original, un-publicized assembly.Repro
SdkLib/Api.cs:Tests/Tests.csproj:Tests/QuotaTests.cs:Actual vs expected
dotnet build→ 0 warnings, 0 errors.dotnet run→ 1/1 passed. Correct.Loading the same project through Roslyn's
MSBuildWorkspace(what OmniSharp and the C# language server use) and asking the compilation for diagnostics:In an editor the whole test is underlined red, and a second error appears where the mock is passed on:
Expected: the IDE agrees with the compiler, since the build is authoritative and correct.
Root cause
TUnit.Mocks.InternalsAccess.targetsdoes this deliberately, and the comment explains why —ReferencePathhas to keep the original so copy-local anddeps.jsonbind to the real assembly at runtime. That reasoning holds for a real build, but it also means no design-time consumer ever sees the publicized copy.Suggested fix
Swap
ReferencePathas well when'$(DesignTimeBuild)' == 'true'. Nothing is copied in a design-time build, so the copy-local /deps.jsonargument does not apply there, and the editor would then see what the compiler sees.If that is not workable, it would be worth a line in the docs for the feature, since the failure mode looks exactly like a broken setup.
Versions
TUnit/TUnit.Mocks1.68.0,net10.0, SDK 10.0.401, macOS arm64TUnit.Mocks.InternalsAccess.targetsis unchanged across those releasesMicrosoft.CodeAnalysis.Workspaces.MSBuild4.14.0 +Microsoft.Build.Locator1.7.8, openingTests.csprojand printingCompilation.GetDiagnostics()errors