单元测试自动映射器

4

我在单元测试中使用automapper遇到了问题。我注入了映射引擎,在代码中可以正常运行,但在测试中无法正常工作。以下是我的测试设置和测试代码。我使用Moq来模拟映射引擎。

private static IDbContext Context { get; set; }
private static Mock<IMappingEngine> Mapper { get; set; }
private static Guid MenuId { get; set; }

private static Guid menuItem1Id { get; set; }
private static Guid menuItem2Id { get; set; }
private static Guid menuItem3Id { get; set; }

[ClassInitialize]
public static void SetUp(TestContext context)
{
    MenuId = Guid.NewGuid();
    Context = new TestDbContext();

    menuItem1Id = Guid.NewGuid();
    menuItem2Id = Guid.NewGuid();
    menuItem3Id = Guid.NewGuid();

    var contentPage1 = new ContentPage { Id = Guid.NewGuid(), ActionName = "ActionName1", ControllerName = "ControllerName1", MenuItemId = menuItem1Id };
    var contentPage2 = new ContentPage { Id = Guid.NewGuid(), ActionName = "ActionName2", ControllerName = "ControllerName2", MenuItemId = menuItem2Id };
    var contentPage3 = new ContentPage { Id = Guid.NewGuid(), ActionName = "ActionName3", ControllerName = "ControllerName3", MenuItemId = menuItem3Id };

    var menuItem1 = new MenuItem { Id = menuItem1Id, MenuId = MenuId, DisplayName = "MenuItem1", ExternalUrl = null, Target = Target._self, Active = true, ContentPage = contentPage1 };
    var menuItem2 = new MenuItem { Id = menuItem1Id, MenuId = MenuId, DisplayName = "MenuItem2", ExternalUrl = null, Target = Target._self, Active = true, ContentPage = contentPage2 };
    var menuItem3 = new MenuItem { Id = menuItem1Id, MenuId = MenuId, DisplayName = "MenuItem3", ExternalUrl = null, Target = Target._self, Active = true, ContentPage = contentPage3 };

    var menu = new Models.Menu { Id = MenuId, Name = "TestMenu", SiteId = Guid.NewGuid(), MenuItems = new List<MenuItem> { menuItem1, menuItem2, menuItem3 } };

    Context.Menus.Add(menu);
    Context.MenuItems.Add(menuItem1);
    Context.MenuItems.Add(menuItem2);
    Context.MenuItems.Add(menuItem3);

    var menuItemQueryResult = new List<MenuItemQueryResult>
    {
        new MenuItemQueryResult { Id = menuItem1Id, DisplayName = "MenuItem1", ExternalUrl = null, Target = Target._self, Active = true, ActionName = "ActionName1", ControllerName = "ControllerName1" },
        new MenuItemQueryResult { Id = menuItem1Id, DisplayName = "MenuItem2", ExternalUrl = null, Target = Target._self, Active = true, ActionName = "ActionName2", ControllerName = "ControllerName2" },
        new MenuItemQueryResult { Id = menuItem1Id, DisplayName = "MenuItem3", ExternalUrl = null, Target = Target._self, Active = true, ActionName = "ActionName3", ControllerName = "ControllerName3" }
    };

    Mapper = new Mock<IMappingEngine>();

    Mapper.Setup(m => m.Map<IEnumerable<MenuItem>, IEnumerable<MenuItemQueryResult>>(It.IsAny<IEnumerable<MenuItem>>()))
                        .Returns(menuItemQueryResult);
}

[TestMethod]
public void Retrieve_RequestMenu_QueryResultReturned()
{
    var handler = new MenuQueryHandler(Context, Mapper.Object);
    var query = new MenuQuery("TestMenu");
    var result = handler.Retrieve(query);
    Assert.IsNotNull(result);
    Assert.IsInstanceOfType(result, typeof(MenuQueryResult));

    var item = result.FirstOrDefault(r => r.Id == menuItem1Id);
    Assert.IsNotNull(item);
}

我现在正在测试的内容如下:

public class MenuQueryHandler : IQueryHandler<MenuQuery, MenuQueryResult>
{
    private IDbContext Context { get; set; }
    private IMappingEngine Mapper { get; set; }

    public MenuQueryHandler(IDbContext context, IMappingEngine mapper)
    {
        Context = context;
        Mapper = mapper;
    }

    public MenuQueryResult Retrieve(MenuQuery query)
    {
        Ensure.Argument.Is(query != null);
        Ensure.Argument.IsNot(query.MenuName == string.Empty);

        // Create the view model query result
        var result = new List<MenuItemQueryResult>();

        // Pull the required item from the cont.ext
        var menuItems = Context.MenuItems.Include(m => m.ContentPage).ToList();

        Mapper.Map(menuItems, result);

        return new MenuQueryResult(result);

    }
}

测试运行了,但映射从未发生。

4
不要嘲笑AutoMapper,我从来没有这么做过。 - Jimmy Bogard
你在测试中使用了实际的 AutoMapper 实例吗?我试图实现的是注入 AutoMapper 引擎(IMappingEngine)。如果我注入它,我需要一个用于测试的实例。你的意思是不要注入 IMappingEngine 吗? - Ethan Schofer
是的。即使您注入了IMappingEngine,我也不会模拟它。我使用真实的东西。 - Jimmy Bogard
2个回答

6

以下是需要注意的几个问题:

  1. You aren't mocking the method you're actually calling. The method you're testing calls this method:

    TDestination Map<TSource, TDestination>(TSource source, TDestination destination);
    

    This overload of Map takes an existing destination object and maps into it.

    In your test, you're mocking the overload of Map that returns a new instance of TDestination:

    TDestination Map<TSource, TDestination(TSource source);
    

    Note that the one you're mocking takes one parameter and the one you're actually calling takes two.

  2. Your Setup method sets up a fake mapping between IEnumerable<MenuItem> and IEnumerable<MenuItemQueryResult>. In your test, you're actually calling Map with a List<MenuItem> and a List<MenuItemQueryResult>.

    In actual usage, AutoMapper is going to handle the List to List mapping for your using the IEnumerable mapping. With a mocked method though, you're not actually calling the method with the exact type parameters you specified. So you'll have to change the Setup call and change the fake mapping.

为了解决这些问题,你可以采取以下两种方法之一:
  1. Change the method to use the overload of Map that returns a new instance.

    It doesn't look like you need to use the overload that takes two parameters, so you could tweak the method:

    var result = Mapper.Map<List<MenuItemQueryResult>(menuItems);
    

    And then in your test:

    Mapper.Setup(m => m.Map<List<MenuItem>, List<MenuItemQueryResult>>(It.IsAny<List<MenuItem>>())
    
    Mapper
        .Setup(m => m.Map<List<MenuItem>, List<MenuItemQueryResult>>(It.IsAny<List<MenuItem>>()))
        .Returns(menuItemQueryResult);
    
  2. Change the test to mock the correct overload of Map.

    This is a little less intuitive, but possible. You'll have to provide a fake implementation of the Map method:

    Mapper
        .Setup(m => m.Map<List<MenuItem>, List<MenuItemQueryResult>>(It.IsAny<List<MenuItem>>(), It.IsAny<List<MenuItem>>()))
        .Callback((List<MenuItem> menuItems, List<MenuItemQueryResult> queryResults) =>
        {
            queryResults.AddRange(menuItemQueryResult);
        });
    

太好了,正是我所需要的。1)测试中的模拟映射对类型更加严格(不能期望List和IEnumerable相等)。2)Map方法的重载必须在模拟和实现之间匹配。现在看到发生了什么,这一切都很明显。 - Ethan Schofer
@EthanSchofer:很高兴能帮忙! - Andrew Whitaker

1
你的模拟映射器设置为返回menuItemQueryResult,但在你的实现中,你没有使用Mapper.Map函数的结果。我认为你应该像这样使用你的映射器:
result = Mapper.Map(menuItems);

编辑:

如果您正在使用Automapper并且已经正确地配置了类型:

result = Mapper.Map<IEnumerable<MenuItem>, List<MenuItemQueryResult>>(menuItems);

那甚至都无法编译。 - Ethan Schofer
我假设你已经自己编写了映射器,在这种情况下请更改实现。如果你正在使用AutoMapper,那么应该是:result = Mapper.Map<IEnumerable<MenuItemQueryResult>>(menuItems); 或者检查你所使用的映射器的详细信息。 - Jimmy Hannon
在测试中返回 null,但在实际网站上运行正常,这让我认为问题出在我的 Moq 设置上。 - Ethan Schofer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接