Moq 你如何(你能?)将两个不同(不兼容)的接口添加到同一上下文中吗?
Moq How do you(Can you?) add two different (incompatible) interfaces to the same context?
我的上下文是我的数据模型的模拟
我在另一个名为 "Email" 的 class 中有一个方法 "Send"
我的服务 class 使用模拟数据模型。
我的服务 class "SendEmailForAlarm" 中的一个方法从 Mock 数据模型访问数据,然后调用电子邮件 Class 中的 "Send" 方法。
问题:如何让我的电子邮件 class 中的 "Send" 方法包含在我的 mock 中?
相关代码:
//INTERFACES
public interface IEmail
{
...
void Send(string from, string to, string subject, string body, bool isHtml = false);
}
public interface IEntityModel : IDisposable
{
...
DbSet<Alarm> Alarms { get; set; } // Alarms
}
//CLASSES
public class Email : IEmail
{
...
public void Send(string from, string to, string subject, string body, bool isHtml = false)
{
...sends the email
}
}
public partial class EntityModel : DbContext, IEntityModel
{
...
public virtual DbSet<Alarm> Alarms { get; set; } // Alarms
}
public class ExampleService : ExampleServiceBase
{
//Constructor
public ExampleService(IEntityModel model) : base(model) { }
...
public void SendEmailForAlarm(string email, Alarm alarm)
{
...
new Email().Send(from, to, subject, body, true);
}
}
//HELPER method
public static Mock<DbSet<T>> GetMockQueryable<T>(Mock<DbSet<T>> mockSet, IQueryable<T> mockedList) where T : BaseEntity
{
mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(mockedList.Expression);
mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(mockedList.ElementType);
mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(mockedList.GetEnumerator());
mockSet.Setup(m => m.Include(It.IsAny<string>())).Returns(mockSet.Object);
// for async operations
mockSet.As<IDbAsyncEnumerable<T>>()
.Setup(m => m.GetAsyncEnumerator())
.Returns(new TestDbAsyncEnumerator<T>(mockedList.GetEnumerator()));
mockSet.As<IQueryable<T>>()
.Setup(m => m.Provider)
.Returns(new TestDbAsyncQueryProvider<T>(mockedList.Provider));
return mockSet;
}
以下单元测试通过但未写入以验证电子邮件是否实际发送(或调用)
service.SendEmailForAlarm 方法查询 EntityModel.Alarms DBSet 并从检索到的对象中提取信息。然后它调用电子邮件中的 Send 方法 Class 和 returns void.
[TestMethod]
public void CurrentTest()
{
//Data
var alarms = new List<Alarm> {CreateAlarmObject()} //create a list of alarm objects
//Arrange
var mockContext = new Mock<IEntityModel>();
var mockSetAlarm = new Mock<DbSet<Alarm>>();
var service = new ExampleService(mockContext.Object);
mockSetAlarm = GetMockQueryable(mockSetAlarm, alarms.AsQueryable());
mockContext.Setup(a => a.Alarms).Returns(mockSetAlarm.Object);
//Action
service.SendEmailForAlarm("test@domain.com", alarms[0]);
//NOTHING IS ASSERTED, SendEmailForAlarm is void so nothing returned.
}
我想做的是在 Email.Send 方法上放置一个拦截器,这样我就可以计算它被执行了多少次,或者让模拟验证验证它被调用了 n 次(在这个例子中是一次)。
[TestMethod]
public void WhatIWantTest()
{
//Data
var alarms = new List<Alarm> {CreateAlarmObject()} //create a list of alarm objects
var calls = 0;
//Arrange
var mockEmail = new Mock<IEmail>(); //NEW
var mockContext = new Mock<IEntityModel>();
var mockSetAlarm = new Mock<DbSet<Alarm>>();
var service = new ExampleService(mockContext.Object);
mockEmail.Setup(u =>
u.Send(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<bool>()))
.Callback(() => calls++); //NEW add a call back on the Email.Send method
mockSetAlarm = GetMockQueryable(mockSetAlarm, alarms.AsQueryable());
mockContext.Setup(a => a.Alarms).Returns(mockSetAlarm.Object);
//Action
service.SendEmailForAlarm("test@domain.com", alarms[0]);
//Assert NEW
Assert.AreEqual(1, calls); //Check callback count to verify send was executed
//OR
//Get rid of method interceptor line above and just verify mock object.
mockEmail.Verify(m => m.Send(It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<bool>()), Times.Once());
}
当我 运行 时,断言因 "Calls" = 0 和 Times.Once 也为零而失败。
我认为我的问题是由于 "service" 是使用访问警报数据所需的 "mockContext" (IEntityModel) 创建的,但 Send 方法不是 mockContext 的一部分。如果我模拟 "mockEmail" 并添加我的回调,我不知道如何将它添加到 mockContext。
如何将 Mock IEmail 和 Mock IEntityModel 添加到同一个上下文中,或者我这样做是不是完全错误?
澄清一下:您正在创建一个 Mock,但是 没有任何东西 可以告诉代码 使用 这个 mock 来做任何事情,来自我能看到什么。您不会在任何地方使用该模拟 - 只需创建它然后再对其进行测试;毫不奇怪,它没有被调用。
我认为你遇到的问题是你的 ExampleService
正在实例化一个新的 Email()
class:
new Email().Send(from, to, subject, body, true);
而更好的方法是在构造函数中提供 IEmail
:
IEmail _email;
//Constructor
public ExampleService(IEntityModel model, IEmail email) : base(model)
{
_email = email;
}
以后再用这个:
public void SendEmailForAlarm(string email, Alarm alarm)
{
_email.Send(from, to, subject, body, true);
}
然后您可以将您的 Mock<IEmail>
注入您的 ExampleService
并检查它是否被适当调用。
如果 difficult/impossible 为构造函数注入正确设置 DI,那么你 可以 有一个 public 属性 允许你将 class 中的 IEmail 替换为另一个用于单元测试,但这有点混乱:
public class ExampleService
{
private IEmail _email = null;
public IEmail Emailer
{
get
{
//if this is the first access of the email, then create a new one...
if (_email == null)
{
_email = new Email();
}
return _email;
}
set
{
_email = value;
}
}
...
public void SendEmailForAlarm(string email, Alarm alarm)
{
//this will either use a new Email() or use a passed-in IEmail
//if the property was set prior to this call....
Emailer.Send(from, to, subject, body, true);
}
}
那么在你的测试中:
//Arrange
var mockEmail = new Mock<IEmail>(); //NEW
var mockContext = new Mock<IEntityModel>();
var mockSetAlarm = new Mock<DbSet<Alarm>>();
var service = new ExampleService(mockContext.Object);
//we set the Emailer object to be used so it doesn't create one..
service.Emailer = mockEmail.Object;
如果有这样的 public
属性 是个问题,您也许可以 internal
并使用 internalsvisibleto
(google) 来解决在您的测试项目中可见(尽管这对包含服务本身的项目中的其他 classes 仍然可见)..
我的上下文是我的数据模型的模拟 我在另一个名为 "Email" 的 class 中有一个方法 "Send" 我的服务 class 使用模拟数据模型。 我的服务 class "SendEmailForAlarm" 中的一个方法从 Mock 数据模型访问数据,然后调用电子邮件 Class 中的 "Send" 方法。 问题:如何让我的电子邮件 class 中的 "Send" 方法包含在我的 mock 中?
相关代码:
//INTERFACES
public interface IEmail
{
...
void Send(string from, string to, string subject, string body, bool isHtml = false);
}
public interface IEntityModel : IDisposable
{
...
DbSet<Alarm> Alarms { get; set; } // Alarms
}
//CLASSES
public class Email : IEmail
{
...
public void Send(string from, string to, string subject, string body, bool isHtml = false)
{
...sends the email
}
}
public partial class EntityModel : DbContext, IEntityModel
{
...
public virtual DbSet<Alarm> Alarms { get; set; } // Alarms
}
public class ExampleService : ExampleServiceBase
{
//Constructor
public ExampleService(IEntityModel model) : base(model) { }
...
public void SendEmailForAlarm(string email, Alarm alarm)
{
...
new Email().Send(from, to, subject, body, true);
}
}
//HELPER method
public static Mock<DbSet<T>> GetMockQueryable<T>(Mock<DbSet<T>> mockSet, IQueryable<T> mockedList) where T : BaseEntity
{
mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(mockedList.Expression);
mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(mockedList.ElementType);
mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(mockedList.GetEnumerator());
mockSet.Setup(m => m.Include(It.IsAny<string>())).Returns(mockSet.Object);
// for async operations
mockSet.As<IDbAsyncEnumerable<T>>()
.Setup(m => m.GetAsyncEnumerator())
.Returns(new TestDbAsyncEnumerator<T>(mockedList.GetEnumerator()));
mockSet.As<IQueryable<T>>()
.Setup(m => m.Provider)
.Returns(new TestDbAsyncQueryProvider<T>(mockedList.Provider));
return mockSet;
}
以下单元测试通过但未写入以验证电子邮件是否实际发送(或调用) service.SendEmailForAlarm 方法查询 EntityModel.Alarms DBSet 并从检索到的对象中提取信息。然后它调用电子邮件中的 Send 方法 Class 和 returns void.
[TestMethod]
public void CurrentTest()
{
//Data
var alarms = new List<Alarm> {CreateAlarmObject()} //create a list of alarm objects
//Arrange
var mockContext = new Mock<IEntityModel>();
var mockSetAlarm = new Mock<DbSet<Alarm>>();
var service = new ExampleService(mockContext.Object);
mockSetAlarm = GetMockQueryable(mockSetAlarm, alarms.AsQueryable());
mockContext.Setup(a => a.Alarms).Returns(mockSetAlarm.Object);
//Action
service.SendEmailForAlarm("test@domain.com", alarms[0]);
//NOTHING IS ASSERTED, SendEmailForAlarm is void so nothing returned.
}
我想做的是在 Email.Send 方法上放置一个拦截器,这样我就可以计算它被执行了多少次,或者让模拟验证验证它被调用了 n 次(在这个例子中是一次)。
[TestMethod]
public void WhatIWantTest()
{
//Data
var alarms = new List<Alarm> {CreateAlarmObject()} //create a list of alarm objects
var calls = 0;
//Arrange
var mockEmail = new Mock<IEmail>(); //NEW
var mockContext = new Mock<IEntityModel>();
var mockSetAlarm = new Mock<DbSet<Alarm>>();
var service = new ExampleService(mockContext.Object);
mockEmail.Setup(u =>
u.Send(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<bool>()))
.Callback(() => calls++); //NEW add a call back on the Email.Send method
mockSetAlarm = GetMockQueryable(mockSetAlarm, alarms.AsQueryable());
mockContext.Setup(a => a.Alarms).Returns(mockSetAlarm.Object);
//Action
service.SendEmailForAlarm("test@domain.com", alarms[0]);
//Assert NEW
Assert.AreEqual(1, calls); //Check callback count to verify send was executed
//OR
//Get rid of method interceptor line above and just verify mock object.
mockEmail.Verify(m => m.Send(It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<bool>()), Times.Once());
}
当我 运行 时,断言因 "Calls" = 0 和 Times.Once 也为零而失败。 我认为我的问题是由于 "service" 是使用访问警报数据所需的 "mockContext" (IEntityModel) 创建的,但 Send 方法不是 mockContext 的一部分。如果我模拟 "mockEmail" 并添加我的回调,我不知道如何将它添加到 mockContext。
如何将 Mock IEmail 和 Mock IEntityModel 添加到同一个上下文中,或者我这样做是不是完全错误?
澄清一下:您正在创建一个 Mock,但是 没有任何东西 可以告诉代码 使用 这个 mock 来做任何事情,来自我能看到什么。您不会在任何地方使用该模拟 - 只需创建它然后再对其进行测试;毫不奇怪,它没有被调用。
我认为你遇到的问题是你的 ExampleService
正在实例化一个新的 Email()
class:
new Email().Send(from, to, subject, body, true);
而更好的方法是在构造函数中提供 IEmail
:
IEmail _email;
//Constructor
public ExampleService(IEntityModel model, IEmail email) : base(model)
{
_email = email;
}
以后再用这个:
public void SendEmailForAlarm(string email, Alarm alarm)
{
_email.Send(from, to, subject, body, true);
}
然后您可以将您的 Mock<IEmail>
注入您的 ExampleService
并检查它是否被适当调用。
如果 difficult/impossible 为构造函数注入正确设置 DI,那么你 可以 有一个 public 属性 允许你将 class 中的 IEmail 替换为另一个用于单元测试,但这有点混乱:
public class ExampleService
{
private IEmail _email = null;
public IEmail Emailer
{
get
{
//if this is the first access of the email, then create a new one...
if (_email == null)
{
_email = new Email();
}
return _email;
}
set
{
_email = value;
}
}
...
public void SendEmailForAlarm(string email, Alarm alarm)
{
//this will either use a new Email() or use a passed-in IEmail
//if the property was set prior to this call....
Emailer.Send(from, to, subject, body, true);
}
}
那么在你的测试中:
//Arrange
var mockEmail = new Mock<IEmail>(); //NEW
var mockContext = new Mock<IEntityModel>();
var mockSetAlarm = new Mock<DbSet<Alarm>>();
var service = new ExampleService(mockContext.Object);
//we set the Emailer object to be used so it doesn't create one..
service.Emailer = mockEmail.Object;
如果有这样的 public
属性 是个问题,您也许可以 internal
并使用 internalsvisibleto
(google) 来解决在您的测试项目中可见(尽管这对包含服务本身的项目中的其他 classes 仍然可见)..