配置AutoMapper以在测试层中返回Mock

时间:2013-06-02 03:20:46

标签: tdd moq automapper

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

  1. 全部实现界面IEntity
  2. Id属性为protected *
  3. 类型IList的属性在构造函数中受到保护和初始化。
  4. 以下是域实体的典型示例:

    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,因此解决方案是模拟Get属性Id的整个对象对应新的Id。这一行究竟是什么stub.Setup(s => s.Id).Returns(generateId)

      

    *按照惯例,Id&lt; = 0的对象是新的,Id&gt; 0是数据库中的现有对象   **对于模拟我使用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,此代码就会运行。对于任何创建新Processo的测试,此代码将被执行!

    测试

    如果我们必须创建一个创建新Processo的测试,我们的第一个目标是使用PassoRepositorio和{{1}填充Passos存储库中的一些虚拟数据* }专门针对上面的代码不会失败**。

      

    *要使用虚拟数据填充对象,我使用AutoFixture   **如果在CheckListItemTemplates存储库中找不到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);    
    

    问题

    我们创建了一个包含10个[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(); } 的列表,其中每个Passo实际上都是Passo,太棒了!但是:

    1. 每个Mock<>都有一个包含5个“模拟”项目的列表,每个Passo应该是1,2,3,4和5(按此顺序)。怎么做到这一点?如何在Id已填充IList<Mock<>>的情况下获取此Mock<>列表?也就是配置 Id

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

1 个答案:

答案 0 :(得分: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<>类。

我希望这些答案中的一个或两个至少能让您深思熟虑。老实说,跟你的整个例子有点困难!