在我的应用程序中,所有域类都遵循标准化:
- 全部实现接口
IEntity
Id
属性是protected
*- 类型的属性
IList
在构造函数中被保护和初始化。
以下是域实体的经典示例:
public class CheckListItemTemplate : IEntity
{
public virtual int Id { get; protected set; }
public virtual string Text { get; set; }
public virtual CheckListItemTemplate Parent { get; set; }
public virtual IList<CheckListItemTemplate> Itens { get; protected set; }
public CheckListItemTemplate()
{
Itens = new List<CheckListItemTemplate>();
}
public void AddItem(CheckListItemTemplate item)
{
item.Parent = this;
Itens.Add(item);
}
}
*这是因为 id 是由数据库生成的,不会冒某些开发人员尝试设置此属性的风险。
测试项目
我们在测试中使用了一个伪造的通用存储库:
public class Repository<T> : IRepository<T>
where T : class, IEntity
{
private readonly IDictionary<int, T> _context = new Dictionary<int, T>();
public void Delete(T obj)
{
_context.Remove(obj.Id);
}
public void Store(T obj)
{
if (obj.Id > 0)
_context[obj.Id] = obj;
else
{
var generateId = _context.Values.Any() ? _context.Values.Max(p => p.Id) + 1 : 1;
var stub = Mock.Get<T>(obj);
stub.Setup(s => s.Id).Returns(generateId);
_context.Add(generateId, stub.Object);
}
}
// ..
}
正如您在Store
* 中看到的,所有测试对象(类型为IEntity
)都应该是Mock
**。这是因为在 UI 项目中,当我们保存对象时 NHibernate 会更新属性Id
。在测试项目中,我们必须手动执行此操作,并且我们无法将属性设置Id
为新值,因此解决方案是将整个对象模拟为与新 Id 对应的Get
属性。Id
这行究竟是做什么的stub.Setup(s => s.Id).Returns(generateId)
。
*按照惯例,Id <= 0 的对象是新的,Id> 0 是数据库中的现有对象。
**对于 Mock 我使用Moq。
Id
作为受保护的
最大的问题是由于Id
财产而发生的,事实就是如此protected
。当我们谈论设计器时,这是一个很好的方法,但是当我们测试我们的应用程序时这会带来巨大的不便。
例如,在我正在编写的测试中,我需要已经填充了一些数据的 Fake 存储库。
代码
跟着我。我有以下课程(+CheckListItemTemplate
如上所示。)
public class Passo : IEntity
{
public int Id { get; protected set; }
public virtual IList<CheckListItemTemplate> CheckListItens { get; protected set; }
}
public class Processo : IEntity
{
public virtual int Id { get; protected set; }
public virtual Passo Passo { get; set; }
public virtual IList<CheckListItem> CheckListItens { get; protected set; }
}
保存后Processo
,第一个Passo
与Processo
: 相关联(按Ordem
字段后字段排序CreateAt
)
model.Passo = PassoRepositorio.All().OrderBy(p => p.Ordem).ThenBy(p => p.CreateAt).First();
model.CheckListItens.Clear();
Parallel.ForEach(Mapper.Map<IList<CheckListItem>>(model.Passo.CheckListItens), (it) => model.AddCheckListItem(it));
每当您保存新的Processo
. 对于任何创建 new 的测试,Processo
都将执行此代码!
测试
如果我们必须创建一个创建新的测试Processo
,我们的第一个目标是使用一些虚拟数据填充PassoRepositorio
存储库*,Passos
并且CheckListItemTemplates
专门针对上述代码不会失败**。
*要使用虚拟数据填充对象,我使用AutoFixture。
** 如果没有Passo
在存储库中找到.First()
并且Passo
没有清单,则将失败Mapper.Map(model.Passo.CheckListItens)
。
所以我们需要一个存储库,Passos
每个存储库都有Passo
一个CheckListItens
. 请记住,每个对象IEntity
都应该是一个Mock<>
,因此我们可以模拟属性Id
首先配置我TestInitialize
用一些虚拟数据填充我的存储库:
var fix = new Fixture();
var listPassos = fix.Build<Mock<Passo>>()
.Do((passo) => {
passo.SetupProperty(x => x.Nome, fix.Create<string>());
passo.SetupGet(x => x.CheckListItens).Returns(
fix.Build<CheckListItemTemplate>() // Needs to a Mock<>
.With(p => p.Texto)
.OmitAutoProperties()
.CreateMany(5).ToList()
);
})
.OmitAutoProperties()
.CreateMany(10);
foreach (var item in listPassos)
passoRepository.Store(item.Object);
然后我可以运行测试:
[TestMethod]
public void Salvar_novo_processo_modificar_data_atendimento_passo_atual()
{
// Arrange
var fix = new Fixture();
var vm = fix.Create<ProcessoViewModel>();
//Act
Controller.salvar(vm); // Problem here. (For convert ProcessoViewModel to Processo I use a AutoMaper. In repository needs destination to be a Mock<Processo>
var processo = Repository.Get(p => p.DataEntrada == vm.DataEntrada && p.ProximoAtendimento == vm.ProximoAtendimento);
//Asserts
processo.Should().NotBeNull();
processo.Passo.Should().NotBeNull();
}
问题
我们创建了一个包含 10 个的列表,Passo
其中每个Passo
实际上是一个Mock<>
,太棒了!但:
每个
Passo
都有一个包含 5 个“模拟”项目的列表,每个项目Id
应该是 1、2、3、4 和 5(按此顺序)。如何做到这一点?如何获取已填充的内部IList<Mock<>>
列表?也就是配置Mock<>
Id
passo.SetupGet(x => x.CheckListItens).Returns( ???
负责在我的控制器中创建对象,基本上是使用 AutoMapper 将我的 ViewModel 对象转换为可以在我的存储库中持久化模型的对象:
model = Mapper.Map<TModel>(vm);
问题是我的存储库 Fake 无法保存对象IEntity
,只是Mock<IEntity>
。如何将 AutoMapper 配置为始终返回一个Mock<>
?