Replacement of Algorithm Testing with Testing of Effects Being Inserted

Total: 2 Average: 4.5

As I expected, Rule 8 from the article “Rules for Implementing TDD in Old Project” stating that we don’t need to test the algorithm of methods raised many “how” and “why” questions. When writing the previous article, it seemed obvious to me, so I did not go into much details on the matter. In this article, I provide a small sample code and two examples of how it could be tested.

The “Do not test the algorithm of methods” from the previous article says:

Some people check the number of calls of certain methods, verify the call itself, etc., in other words, check the internal work of methods. It’s just as bad as testing of the private ones. The difference is only in the application layer of such check. This approach again gives a lot of fragile tests, thus some people do not take TDD properly”.

There is a handler code:

public class SomeEventHandler
{
    public SomeEventHandler(IDatabaseCommands dbCommands,
                            IEventValidator validator,
                            IMessagingLogger messagingLogger)
    {
        // skipped
    }

    public HandlerResult Handle(EventPayload payload)
    {
        if (Validator.IsOurEvent(payload))
            if (Validator.IsValid(payload))
            {
                var evt = Mapper.Map<Event>(payload);
                try
                {
                    using (var tran = new TransactionScope())
                    {
                        DbCommands.SaveEvt(evt);
                        MessagingLogger.Received(payload);
                        tran.Complete();
                    }
                }
                catch (Exception ex)
                {
                    return MessageHandlerResult.Fatal;
                }
            }
            else
            {
                var error = Validator.GetErrors();
                MessagingLogger.InvalidEvent(payload, error);
                return MessageHandlerResult.Fatal;
            }
        return MessageHandlerResult.Success;
    }
}

It is necessary to test the Handle() method. Make sure that the DbCommands and MessagingLogger methods were called.

“Mock” Approach

It would pass the mocks of the corresponding interfaces to the class constructor, and then check whether the corresponding methods were called: SaveEvt(), Received(), or InvalidEvent(). The code would look something like this:

public void Should_save_valid_data_and_log_to_messaging_events()
{
    var builder = new EventPayload {
        // skipped
    };

    var validator = Mock.Of<IEventValidator>();
    var dbCommands = new Mock<IDatabaseCommands>();
    var messagingLogger = new Mock<IMessagingLogger>();
    var handler = new SomeEventHandler(dbCommands, validator, messagingLogger);

    var result = handler.Handle(payload);

    // assertions
    Assert.Equal(MessageHandlerResult.Success, result);
    dbCommands.Verify(m => m.SaveEvt(It.IsAny<Event>(), Times.Once())
    messagingLogger.Verify(m => m.Received(It.IsAny<EventPayload>(), Times.Once())
}

“Non-mock” Approach

It would create fake objects and check whether the event and not the method call, occurred. In this case, the code would be something like:

public void Should_save_valid_data_and_log_to_messaging_events()
{
    var builder = new EventPayload {
        // skipped
    };

    var validator = Mock.Of<IEventValidator>();
    var dbCommands = new FakeDatabaseCommands();
    var messagingLogger = new FakeMessagingLogger();
    var handler = new SomeEventHandler(dbCommands, validator, messagingLogger);

    var result = handler.Handle(payload);

    // assertions
    Assert.Equal(MessageHandlerResult.Success, result);
    Assert.True(dbCommands.IsEventSaved);
    Assert.True(messagingLogger.IsEventRegistered);
}

And the fake-objects methods would look like this:

public void SaveEvt(Event evt)
{
    IsEventSaved = true;
}

IsEventSaved would be declared only in a fake object.

Advantages and disadvantages

The first approach is simple and fast, but if you need to change methods, call one instead of the other in the same situation, then it would be necessary to modify the tests.

The second approach leads to the creation of additional entities, and you get the benefit only in the situation with the replacement of methods. In this case, perhaps, you will not even have to change anything in Fakes, or in tests. Another advantage, more idealistic, is that you make the test so that it does not know about the internals of the test method. Therefore, personally I, if time permits, do tests for fakes.