1

在我的应用程序中,所有域类都遵循标准化:

  1. 全部实现接口IEntity
  2. Id属性是protected*
  3. 类型的属性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,第一个PassoProcesso: 相关联(按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<>,太棒了!但:

  1. 每个Passo都有一个包含 5 个“模拟”项目的列表,每个项目Id应该是 1、2、3、4 和 5(按此顺序)。如何做到这一点?如何获取已填充的内部IList<Mock<>>列表?也就是配置 Mock<>Idpasso.SetupGet(x => x.CheckListItens).Returns( ???

  2. 负责在我的控制器中创建对象,基本上是使用 AutoMapper 将我的 ViewModel 对象转换为可以在我的存储库中持久化模型的对象: model = Mapper.Map<TModel>(vm);
    问题是我的存储库 Fake 无法保存对象IEntity,只是Mock<IEntity>。如何将 AutoMapper 配置为始终返回一个Mock<>?

4

1 回答 1

0

问题 1 的答案:如果这有帮助,您可以使用闭包来维护运行计数器以用作 id。例如:

class MyTestClass
{
  int _runningCounter = 0;

  public void SomeTest()
  {
    /* ... some other code including mock creation ...*/

    someMock.Setup(m => m.ReturnNewWidgetEntity())
      .Returns(() => new WidgetEntity{ Id= ++_runningCounter });
  }
}

每次ReturnNewWidgetEntity在模拟对象上调用时,Id 属性将设置为增加的数字。

问题 2 的答案:我建议不要对Mapper类有具体依赖,而是将其替换为对IMapperEngine. Richard Dingwall 在这里解释了这项技术:http ://richarddingwall.name/2009/05/07/mocking-out-automapper-with-dependency-injection/

基本上,您在容器中注册Mapper.Engine为单例实现,IMapperEngine然后在单元测试中模拟它,以便它为您提供所需的Mock<>类。

我希望这些答案中的一个或两个至少能让你深思。老实说,要效仿你的整个榜样有点困难!

于 2013-09-20T22:56:39.927 回答