moq icon indicating copy to clipboard operation
moq copied to clipboard

Mock.Protected().Verify fails sometimes

Open TheBigNeo opened this issue 1 year ago • 2 comments

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

TheBigNeo avatar Mar 11 '24 11:03 TheBigNeo

Due to lack of recent activity, this issue has been labeled as 'stale'. It will be closed if no further activity occurs within 30 more days. Any new comment will remove the label.

github-actions[bot] avatar May 06 '25 01:05 github-actions[bot]

Still open

TheBigNeo avatar May 06 '25 07:05 TheBigNeo