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
沒有留言:
張貼留言