Tuesday, September 3, 2019

[moq] Mock a non-interface object


 C#   Unit test   Moq 


▌Introduction


We often mock the interface with Moq for unit test.
This article will show how to mock the non-interface object (Class), but there are some preconditions:

1.  Method being mocked must be Virtual
2.  The mocked object’s constructor’s parameter(s) is(are) MUST



▋Related articles




▌Environment


▋Visual Studio 2017 community
▋dotnet core 2.2.203
▋Moq 4.11.0




▌Implement


Followed by the previous tutorial, [moq] Mock object and Mock.of.
Assume we would like to test the method: GetAverageAge(DateTime[] birthdays), in BirthdayRepository:

▋BirthdayRespository.cs

public class BirthdayRepository : IBirthdayRepository
{
        private IDataAccessService<Birthday> _dataAccess =null;
        private ILogger _logger = null;
        private AgeConverter _ageConverter = null;

        public BirthdayRepository(
            IDataAccessService<Birthday> dataAccess,
            ILogger logger,
            AgeConverter ageConverter
        )
        {
            if(dataAccess!=null) this._dataAccess = dataAccess;
            if(logger!=null) this._logger = logger;
            if (ageConverter != null) this._ageConverter = ageConverter;
        }

        public decimal GetAverageAge(DateTime[] birthdays)
        {
            var ages = birthdays.ToList().Select(x => this._ageConverter.CalculateAge(x)).ToList();
            decimal averageAge = Math.Round((decimal)ages.Sum() / (decimal)ages.Count(), 2);
            return averageAge;
        }

        public void Dispose()
        {

        }
}


GetAverageAge has one dependency: AgeConverter, which is a class and it DOES NOT implement any INTERFACE:

▋AgeConverter.cs

public class AgeConverter
{
        private object _foo = null;

        public AgeConverter(object foo)
        {
            this._foo = foo;
        }
        public decimal CalculateAge(DateTime birthday)
        {
            var now = DateTime.Now;
            DateTime zeroTime = new DateTime(1, 1, 1);
            TimeSpan span = DateTime.Now - birthday;
            int years = (zeroTime + span).Year - 1;
            return years;
        }
}


So we start writing Unit Test and try to mock the object: AgeConverter.


▋Unit Test (with errors)

public class UnitTestBirthdayRepository
    {
        [Test]
        public void TestGetAverageAge()
        {
            var birthdays = new DateTime[]
            {
                new DateTime(2001, 01, 18),
                new DateTime(2003, 05, 18),
                new DateTime(2005, 10, 18)
            };

            decimal expected = Math.Round(((decimal)(17 + 15 + 14) / 3M), 2);
            decimal actual = 0;

            #region Mock
            var mockLogger = new Mock<ILogger>();
            var mockDataAccess = new Mock<IDataAccessService<Birthday>>();
            var mockAgeConverter = new Mock<AgeConverter>();
            IBirthdayRepository birthdayRepository = new BirthdayRepository(
                mockDataAccess.Object, mockLogger.Object, mockAgeConverter.Object);
            #endregion

            mockAgeConverter.Setup(x => x.CalculateAge(It.Is<DateTime>(input => input.Equals(birthdays[0])))).Returns(17);
            mockAgeConverter.Setup(x => x.CalculateAge(It.Is<DateTime>(input => input.Equals(birthdays[1])))).Returns(15);
            mockAgeConverter.Setup(x => x.CalculateAge(It.Is<DateTime>(input => input.Equals(birthdays[2])))).Returns(14);

            actual = birthdayRepository.GetAverageAge(birthdays);

            Assert.AreEqual(expected, actual);
        }
    }



The Unit Test will fail with the following errors,

1. Could not find a parameterless constructor



To solve the problem, we have to pass the parameter for the constructor of AgeConverter class.

var mockAgeConverter = new Mock<AgeConverter>(null); // Input parameter is MUST



2. Non-overridable members (here: AgeConverter.CalculateAge) may bit be used in setup / verification expressions



That means we have to set the method: AgeConverter.CalculateAge to be Virtual function.

public class AgeConverter
    {
        private object _foo = null;

        public AgeConverter(object foo)
        {
            this._foo = foo;
        }
        public virtual decimal CalculateAge(DateTime birthday)
        {
            var now = DateTime.Now;
            DateTime zeroTime = new DateTime(1, 1, 1);
            TimeSpan span = DateTime.Now - birthday;
            int years = (zeroTime + span).Year - 1;
            return years;
        }
    }



▋Unit Test (Success ones)

[Test]
public void TestGetAverageAge_V1()
{
            var birthdays = new DateTime[]
            {
                new DateTime(2001, 01, 18),
                new DateTime(2003, 05, 18),
                new DateTime(2005, 10, 18)
            };

            decimal expected = Math.Round(((decimal)(17 + 15 + 14) / 3M), 2);
            decimal actual = 0;

            #region Mock interfaces
            var mockLogger = new Mock<ILogger>();
            var mockDataAccess = new Mock<IDataAccessService<Birthday>>();
            var mockAgeConverter = new Mock<AgeConverter>(null); // Input parameter is MUST
            IBirthdayRepository birthdayRepository = new BirthdayRepository(
                mockDataAccess.Object, mockLogger.Object, mockAgeConverter.Object);
            #endregion

            mockAgeConverter.Setup(x => x.CalculateAge(It.Is<DateTime>(input => input.Equals(birthdays[0])))).Returns(17);
            mockAgeConverter.Setup(x => x.CalculateAge(It.Is<DateTime>(input => input.Equals(birthdays[1])))).Returns(15);
            mockAgeConverter.Setup(x => x.CalculateAge(It.Is<DateTime>(input => input.Equals(birthdays[2])))).Returns(14);

            actual = birthdayRepository.GetAverageAge(birthdays);

            Assert.AreEqual(expected, actual);
}

[Test]
public void TestGetAverageAge_V2()
{
            var birthdays = new DateTime[]
            {
                new DateTime(2001, 01, 18),
                new DateTime(2003, 05, 18),
                new DateTime(2005, 10, 18)
            };

            decimal expected = Math.Round(((decimal)(17 + 15 + 14) / 3M), 2);
            decimal actual = 0;

            #region Mock interfaces
            var mockLogger = new Mock<ILogger>();
            var mockDataAccess = new Mock<IDataAccessService<Birthday>>();
            var mockAgeConverter = new Mock<AgeConverter>(null); // Input parameter is MUST
            IBirthdayRepository birthdayRepository = new BirthdayRepository(
                mockDataAccess.Object, mockLogger.Object, mockAgeConverter.Object);
            #endregion

            mockAgeConverter.SetupSequence(x => x.CalculateAge(It.IsAny<DateTime>())).Returns(17).Returns(15).Returns(14);

            actual = birthdayRepository.GetAverageAge(birthdays);

            Assert.AreEqual(expected, actual);
}




▌Reference




No comments:

Post a Comment