2019年9月3日 星期二

[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




沒有留言:

張貼留言