bUnit-dev / bUnit

bUnit is a testing library for Blazor components that make tests look, feel, and runs like regular unit tests. bUnit makes it easy to render and control a component under test’s life-cycle, pass parameter and inject services into it, trigger event handlers, and verify the rendered markup from the component using a built-in semantic HTML comparer.
https://bunit.dev
MIT License
1.11k stars 104 forks source link

Unable to resolve service for type 'Bunit.Rendering.ITestRenderer' #1472

Closed rob-king-volpara closed 2 months ago

rob-king-volpara commented 2 months ago

I have updated my test project from 1.26.64 to 1.28.9 and now I get an exception when test run complaining "Unable to resolve service for type 'Bunit.Rendering.ITestRenderer'"

EDIT: by incrementally updating the package, it looks like the issue was introduced in 1.27.17

No changes were made to my project apart from updating the package version.

The code that triggers the exception is a TestNavigationManager stub that we use to mock nav manager stuff.

public class TestNavigationManager : NavigationManager
{
    private readonly ITestRenderer _renderer;

    public TestNavigationManager(ITestRenderer renderer)
    {
        Initialize("https://localhost/", "https://localhost/");
        _renderer = renderer;
    }

    protected override void NavigateToCore(string uri, bool forceLoad)
    {
        Uri = ToAbsoluteUri(uri).ToString();

        _renderer.Dispatcher.InvokeAsync(() => NotifyLocationChanged(isInterceptedLink: false));
    }
}

The test...

    [TestInitialize]
    public void TestInitialize()
    {
        _testContext.Services.AddScoped<ILoggingRepository>(_ => Mock.Of<ILoggingRepository>());
        _testContext.Services.AddScoped<UnexpectedErrorService>();
    }

    [TestMethod]
    public Task ErrorBody_UnexpectedExceptionWithCorrelationId_RendersCorrectlyWithDetails()
    {
        // Arrange
        UnexpectedException exception = new()
        {
            CorrelationId = Guid.Parse("9de73937-eb78-4d07-9c5a-1abb4d590f80")
        };

        // Act
        IRenderedComponent<ErrorBody> component = _testContext?.RenderComponent<ErrorBody>(
            p => p.Add(x => x.Exception, exception));

        // Assert
        return Verify(component);
    }

The test nav manager is wired into the TestContext like so:

_testContext.Services.AddSingleton<NavigationManager, TestNavigationManager>();

Results in this output:

 ErrorBody_UnexpectedExceptionWithCorrelationId_RendersCorrectlyWithDetails
   Source: ErrorBodyTests.cs line 20
   Duration: 855 ms

  Message: 
Test method ORGNAME.Components.ErrorBodyTests.ErrorBody_UnexpectedExceptionWithCorrelationId_RendersCorrectlyWithDetails threw exception: 
System.InvalidOperationException: Unable to resolve service for type 'Bunit.Rendering.ITestRenderer' while attempting to activate 'ORGNAME.TestNavigationManager'.

  Stack Trace: 
CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain)
CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain, Int32 slot)
CallSiteFactory.TryCreateExact(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
CallSiteFactory.GetCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain)
CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain, Int32 slot)
CallSiteFactory.TryCreateExact(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
CallSiteFactory.GetCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
ServiceProviderEngineScope.GetService(Type serviceType)
TestServiceProvider.GetServiceInternal(Type serviceType) line 159
TestServiceProvider.GetService(Type serviceType) line 151
ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
Renderer.ctor(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IComponentActivator componentActivator)
TestRenderer.ctor(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory) line 82
WebTestRenderer.ctor(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory) line 22
TestContext.CreateTestRenderer() line 93
TestContextBase.get_Renderer() line 22
TestContextBaseRenderExtensions.RenderInsideRenderTree(TestContextBase testContext, RenderFragment renderFragment) line 43
TestContextBaseRenderExtensions.RenderInsideRenderTree[TComponent](TestContextBase testContext, RenderFragment renderFragment) line 23
TestContext.Render[TComponent](RenderFragment renderFragment) line 68
TestContext.RenderComponent[TComponent](Action`1 parameterBuilder) line 54
ErrorBodyTests.ErrorBody_UnexpectedExceptionWithCorrelationId_RendersCorrectlyWithDetails() line 29
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Expected behavior:

The tests should all pass as they did with the previous version.

Version info:

Link to reproduction repo. Change the package version in the TestProject1.csproj to see it break. https://github.com/rob-king-volpara/bunit-issue-1472

rob-king-volpara commented 2 months ago

Looking through the commits to 1.27, I think it stems from the KeyedServiceProvider inclusion: https://github.com/bUnit-dev/bUnit/pull/1405/files#diff-2d39f5d38f8ec09b9d97543e177f6bc88b04440e13e090d0311b02a67101c970

If I change my code to

_testContext.Services.AddKeyedSingleton<NavigationManager, TestNavigationManager>("TestNavigationManager");

it works again.

linkdotnet commented 2 months ago

Hey @rob-king-volpara That is unfortunate but there is a workaround for that. The file you provided is somewhat unrelated - it just enables keyed services that were introduced with net8.0. Your code might work because your TestNavigationManager isn't used in that case. Do you have anywhere a FromKeyedServices("TestNavigationManager")?

The reason is here: https://github.com/bUnit-dev/bUnit/commit/8f99afa6269b16439b33c400ef07d64d3f3a9b38

We moved the implementation of the ITestRenderer and ITestRenderer itself outside of the DI container.

Instead what you can do is to inject the TestContextBase in your TestNavigationManager (see https://github.com/bUnit-dev/bUnit/blob/main/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs#L30):

public class TestNavigationManager : NavigationManager
{
    private readonly TestContextBase _testContext;

    public TestNavigationManager(TestContextBase testContext)
    {
        Initialize("https://localhost/", "https://localhost/");
        _testContext = testContext;
    }

    protected override void NavigateToCore(string uri, bool forceLoad)
    {
        Uri = ToAbsoluteUri(uri).ToString();

        _testContext.Renderer.Dispatcher.InvokeAsync(() => NotifyLocationChanged(isInterceptedLink: false));
    }
}
rob-king-volpara commented 2 months ago

Ahhh nice one.... I'll give it a try

rob-king-volpara commented 2 months ago

Cool..... I found I no longer need our TestNavigationManager since the FakedNavigationManager works ok for our cases.

Given this isn't really a bug per se, it can probably be closed.