Search code examples
c#unit-testingxunit

how to cover both constructor argument null exception and catch block for method `DoMethod()`


I have below unit test C# code,

public class ServiceTest
{
    public readonly Service _sut;
    private readonly Mock<IServiceClient> _serviceClientMock = new Mock<IServiceClient>();
    private readonly Mock<ILogger<Service>> _loggerMock = new Mock<ILogger<Service>>();
    public ServiceTest()
    {
        _sut = new Service(_serviceClientMock.Object, _loggerMock.Object);
    }

    [Fact]
    public void Constructor_throws_Exception()
    {
        Assert.Throws<ArgumentNullException>(() => new Service(null, null));
    }

    [Fact]
    public async Task Do_Test_For_DoMethod()
    {
        await _sut.DoMethod();
    }
}

I have Constructor_throws_Exception which only covers one argument null exception, but not the other. How to cover both argument null exception plus the catch block for method? Is there a way I can merge with all in a single test? I am using xUnit.

enter image description here


Solution

  • You have to create a unique test for each invalid combination. Could be something like this:

    public static IEnumerable<object[]> GetInvalidConstructorArguments()
    {
        yield return new object[] { new Mock<IServiceClient>().Object, null };
        yield return new object[] { null, new Mock<ILogger<Service>>().Object };
    }
    
    [Theory]
    [MemberData(nameof(GetInvalidConstructorArguments))]
    public void ThrowsOnNullArgument(IServiceClient serviceClient, ILogger<Service> logger)
    {
        Assert.Throws<ArgumentNullException>(() => new Service(serviceClient, logger));
    }
    

    Getting a working mock for the ILogger<> is more complicated then it seems in the first spot. The problem is, that all convenient methods are extensions methods, which can't be mocked. Under the hood, all of these methods will call the Log<TState>() method which must be mocked. Thankfully to this answer, this can be done as follows:

    public class MyTests
    {
        [Fact]
        public void ExceptionShouldBeWrittenToLog()
        {
            // Instruct service client to throw exception when being called.
            var serviceClient = new Mock<IServiceClient>();
            var exception = new InvalidOperationException($"Some message {Guid.NewGuid()}");
            serviceClient.Setup(s => s.Do()).Throws(exception);
            // Create a strict mock, that shows, if an error log should be created.
            var logger = new Mock<ILogger<MyService>>(MockBehavior.Strict);
            logger.Setup(l => l.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((o, t) => o.ToString() == exception.Message),
                It.IsAny<InvalidOperationException>(),
                It.IsAny<Func<It.IsAnyType, Exception, string>>()));
    
            // Setup SUT and call method.
            var service = new MyService(serviceClient.Object, logger.Object);
            service.DoSomething();
    
            // Check if method of logger was being called.
            logger.VerifyAll();
        }
    }
    
    public interface IServiceClient
    {
        public void Do();
    }
    
    public class MyService
    {
        private readonly IServiceClient serviceClient;
        private readonly ILogger<MyService> logger;
    
        public MyService(IServiceClient serviceClient, ILogger<MyService> logger)
        {
            this.serviceClient = serviceClient;
            this.logger = logger;
        }
    
        public void DoSomething()
        {
            try
            {
                serviceClient.Do();
            }
            catch (Exception ex)
            {
                logger.LogError(ex.Message);
            }
        }
    }