Skip to content

Mock.Protected().Verify fails sometimes #1464

Open
@TheBigNeo

Description

@TheBigNeo

Describe the Bug

I want to use Mock.Protected().Verify to check that my function (WatchdogInternalAsync) has been executed.

If I run the unit test (StartWatchdog_WatchdogInternal_FunctionIsCalled1) individually, the test is successful.
However, if another test runs before it, the test fails.
If the same test (StartWatchdog_WatchdogInternal_FunctionIsCalled2) is run again afterward, it only fails sometimes.
And if I call my function twice (in StartWatchdog_WatchdogInternal_FunctionIsCalled3) and check for Times.Exactly(2), then this test is always successful.

I have the feeling it is a timing problem.
When I change the names of the test functions, causing them to run in a different order, I sometimes get other tests failing.

Steps to Reproduce

  • Run all tests

Expected Behavior

All unit tests should be successful

Exception with Stack Trace

Moq.MockException : 
Expected invocation on the mock once, but was 0 times: mock => mock.WatchdogInternalAsync()

Performed invocations:
   Mock<AbstractGrpcClientConnectionWithWatchdog<ApiService.ApiServiceClient>:3> (mock):
      AbstractGrpcClientConnectionWithWatchdog<ApiService.ApiServiceClient>.WatchdogInternalAsync()

Version Info

  • Moq 4.20.70
  • NUnit 4.1.0

Code

Project:
MoqAbstractProtected.zip

C# Code
#nullable disable

using NUnit.Framework;
using Moq;
using Moq.Protected;

namespace MoqAbstractProtected;

public class Tests
{
    private const string WatchdogInternalAsync = "WatchdogInternalAsync";

    private Mock<AbstractClass> ConnectionMock;
    private AbstractClass       Connection;

    [SetUp]
    public void Setup()
    {
        ConnectionMock          = new Mock<AbstractClass>();
        ConnectionMock.CallBase = true;

        Connection = ConnectionMock.Object;

        ConnectionMock.Protected()
            .Setup<Task>(WatchdogInternalAsync)
            .Callback(() => Console.Out.WriteLine($"\"{WatchdogInternalAsync}\" was called!"))
            .Returns(Task.CompletedTask);
    }

    [Test]
    public void StartWatchdog_InitNotCalled_ThrowsException()
    {
        // Prepare
        // Connection.Init(); NO init call

        // Test
        Exception exception = Assert.Throws<Exception>(
            delegate
            {
                // ReSharper disable once AssignNullToNotNullAttribute
                Connection.StartWatchdog();
            });

        // Assert
        Assert.That(exception,         Is.Not.Null);
        Assert.That(exception.Message, Is.EqualTo("The Init function must be called first."));
    }

    [Test]
    public void StartWatchdog_ConnectionIsDisposed_WatchdogInternalNotCalled()
    {
        // Prepare
        Connection.Init();
        Connection.Dispose();

        // Test
        Assert.DoesNotThrow(() => { Connection.StartWatchdog(); });

        // Assert
        ConnectionMock.Protected()
            .Verify(WatchdogInternalAsync, Times.Never());
    }

    [Test]
    public void StartWatchdog_WatchdogInternal_FunctionIsCalled1()
    {
        // Prepare
        Connection.Init();

        // Test
        Connection.StartWatchdog();

        // Assert
        ConnectionMock.Protected()
            .Verify(WatchdogInternalAsync, Times.Once());
    }

    [Test]
    public void StartWatchdog_WatchdogInternal_FunctionIsCalled2()
    {
        // Prepare
        Connection.Init();

        // Test
        Connection.StartWatchdog();

        // Assert
        ConnectionMock.Protected()
            .Verify(WatchdogInternalAsync, Times.Once());
    }

    [Test]
    public void StartWatchdog_WatchdogInternal_FunctionIsCalled3()
    {
        // Prepare
        Connection.Init();

        // Test
        Connection.StartWatchdog();
        Connection.StartWatchdog();

        // Assert
        ConnectionMock.Protected()
            .Verify(WatchdogInternalAsync, Times.Exactly(2));
    }
}
namespace MoqAbstractProtected;

public abstract class AbstractClass : IDisposable
{
    private readonly CancellationTokenSource CancellationTokenSource;
    protected        CancellationToken       CancellationToken { get; }
    protected        bool                    InitDone          { get; private set; }

    protected AbstractClass()
    {
        CancellationTokenSource = new CancellationTokenSource();
        CancellationToken       = CancellationTokenSource.Token;
    }

    public void Init()
    {
        Console.Out.WriteLine("Init");

        if (InitDone)
        {
            throw new Exception("Init already called. Dispose this instance and create a new one.");
        }

        InitDone = true;
    }

    public void StartWatchdog()
    {
        if (InitDone is false)
        {
            throw new Exception($"The {nameof(Init)} function must be called first.");
        }

        Task.Factory.StartNew(WatchdogAsync, CancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    }

    private async Task WatchdogAsync()
    {
        if (CancellationToken.IsCancellationRequested)
        {
            return;
        }

        await WatchdogInternalAsync(); // This is a blocking call
        Console.Out.WriteLine("Watchdog has stopped monitoring. Wuff!");
    }

    protected abstract Task WatchdogInternalAsync();

    protected virtual void Dispose(bool disposing)
    {
        Console.Out.WriteLine($"Dispose({disposing})");

        if (disposing)
        {
            CancellationTokenSource.Cancel();
            CancellationTokenSource.Dispose();
        }
    }

    public void Dispose()
    {
        Dispose(true);

        Console.Out.WriteLine("Dispose");
        GC.SuppressFinalize(this);
    }
}

Back this issue
Back this issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions