Mocking System.Threading.Timer

5 posts, 0 answers
  1. Stefano
    Stefano avatar
    4 posts
    Member since:
    Feb 2019

    Posted 21 Feb 2019 Link to this post

    Hi guys,

    I need to test the handler of a Timer in one class. I'm using Future Mocking to mock the Timer inside it.
    Is there some way in order to invoke the callback?

    Here my class to test:

     

    internal class DatabaseCleaner
    {
     
          private Timer mobjTimer = null;
     
          internal void Start()
          {
     
                mobjTimer = new Timer(CleanDB, null, 5000, 1000 * 60 * 10);
          }
     
          internal void Stop()
          {
             if (mobjTimer != null)
             {
                mobjTimer.Dispose();
                mobjTimer = null;
             }
          }
     
          private void CleanDB(object state)
          {

           // do stuff to test

            }
    }


    Thanks a lot.

    Stefano

  2. Mihail
    Admin
    Mihail avatar
    263 posts

    Posted 22 Feb 2019 Link to this post

    Hi Stefano,

    Yes, you can invoke the private method of an internal class. As you haven't mentioned, I will assume that the internal class DatabaseCleaner is defined in a different assembly than where the test is defined. If this is the case, then what you should do is obtain an instance of that internal class.

    Here is an example of how this could be done:
    var typeInfo = Assembly.Load("ClassLibrary").DefinedTypes.Where((t) => t.FullName == "ClassLibrary.DatabaseCleaner").FirstOrDefault();
    var type = typeInfo.AsType();
    var instance = Activator.CreateInstance(type);

    After that, you should create a PrivateAccessor with an argument the created instance and call the required method. Here is an example:
    object state = 10;
    PrivateAccessor privateAccessor = new PrivateAccessor(instance);
    privateAccessor.CallMethod("CleanDB", state);

    If the test scenario requires it you could arrange the call to CleanDB to executed just once or the appropriate amount. Here an example of how a full test for this method could look like:
    [TestMethod]
    public void TestMethod()
    {
        // Arrange
        var typeInfo = Assembly.Load("ClassLibrary").DefinedTypes.Where((t) => t.FullName == "ClassLibrary.DatabaseCleaner").FirstOrDefault();
        var type = typeInfo.AsType();
        var instance = Activator.CreateInstance(type);
     
     
        object state = 10;
        Mock.NonPublic.Arrange(type, "CleanDB", state).CallOriginal().OccursOnce();
     
        // Act
        Mock.NonPublic.MakePrivateAccessor(instance).CallMethod("CleanDB", state);
     
        // Assert
        Mock.Assert(instance);
    }

    I hope that this information answers your question.

    Regards,
    Mihail
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  3. Stefano
    Stefano avatar
    4 posts
    Member since:
    Feb 2019

    Posted 25 Feb 2019 in reply to Mihail Link to this post

    Hi Mihail,

     

    Thanks for the reply.

    The assembly of the test is the same of my class. Sorry, I forgot to tell you.

    I saw your solution, but is there come way to mock the timer? Because in your way I'm not testing the timer feature.. but I'm just testing a private method.

    Thanks,

    Stefano

  4. Mihail
    Admin
    Mihail avatar
    263 posts

    Posted 25 Feb 2019 Link to this post

    Hi Stefano,

    It seems I didn't understand your question correctly. Please excuse me.

    You should use future mocking for the timer. However, I am afraid that your exact scenario with the private callback CleanDB is not supported. I have logged a new feature request of your behalf. Here is a link if you would like to follow the item and receive status updates: Future mocking of public class with non-public arguments

    I am not sure if this is an option for you but I will share it anyway. If you modify the access modifier of the callback CleanDB to "internal" then the test case will be possible. The reason behind this is that the future mocking depends on expressions and in order for the expression to be compiled successfully all elements of that expression should be visible to the test method.

    Here is how the test method will look like in this case:
    [TestMethod]
    public void TestMethod()
    {
        bool isProcessed = false;
     
        // Arrange
        var dbCleaner = Mock.Create<DatabaseCleaner>(Behavior.CallOriginal);
        Mock.Arrange(() => new Timer(dbCleaner.CleanDB, null, 5000, 1000 * 60 * 10))
            .DoInstead(() => new Timer((o)=> isProcessed = true, null, 0, 0));
     
        // Act
        dbCleaner.Start();
        Thread.Sleep(1000);
     
        // Assert
        Assert.AreEqual(true, isProcessed);
    }

    Please have in mind that in this test I am asserting that the callback is called by the timer. I am also triggering the execution of the callback faster than the original code. I am doing this to speed up the test and it is based on the assumption that the Timer will work as expected regardless of the exact arguments for triggering the callback.

    If you would like to assert that the CleanDB is executed then you should find a way to verify that depending on the job done inside that method. Here is how the arrangement should look like in this case:
    Mock.Arrange(() => new Timer(dbCleaner.CleanDB, null, 5000, 1000 * 60 * 10))
        .DoInstead(() => new Timer(dbCleaner.CleanDB, null, 0, 0));

    I hope this helps.

    Regards,
    Mihail
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  5. Ivo
    Admin
    Ivo avatar
    27 posts

    Posted 25 Jun 2019 Link to this post

    Hello Stefano,

    I hope you are doing well. I would like to let you know about the available options we have found which can solve the issue using the current JustMock API level.

    The first option verifies the exact call to the private will be made. In this case you can create delegate and bind the instance method DatabaseCleaner.CleanDB to it. Than the result delegate can be used as an argument matcher in the arrangement. Here is the code:

    [TestMethod]
    public void TestMethod2()
    {
        // Arrange
        var sut = Mock.Create<DatabaseCleaner>(Behavior.CallOriginal);
        var dbCleanerTimerCallback = (TimerCallback)Delegate.CreateDelegate(typeof(TimerCallback), sut, "CleanDB");
     
        Mock.Arrange(() => new Timer(dbCleanerTimerCallback, null, 5000, 1000 * 60 * 10))
            .IgnoreInstance()
            .MustBeCalled();
     
        // Act
        var dbCleaner = new DatabaseCleaner();
        dbCleaner.Start();
     
        // Assert
        Mock.Assert(sut);
    }
    The second (more generic) option verifies that the delegate of a given type will be called. As an opposite for the previous option, here the argument matcher looks for the specified type, but not for the particular instance. The test looks like as following:

    [TestMethod]
    public void TestMethod3()
    {
        // Arrange
        var sut = Mock.Create<DatabaseCleaner>(Behavior.CallOriginal);
     
        Mock.Arrange(() => new Timer(Arg.IsAny<TimerCallback>(), Arg.AnyObject, Arg.AnyInt, Arg.AnyInt))
            .IgnoreInstance()
            .MustBeCalled();
     
        // Act
        var dbCleaner = new DatabaseCleaner();
        dbCleaner.Start();
     
        // Assert
        Mock.Assert(sut);
    }
     
    It would be nice to share whether these solutions are good enough for you, so we can plan further activities on the related feature request.


    Regards,
    Ivo
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
Back to Top