8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

如何对使用 IMappingAction 的 AutoMapper 配置文件进行单元测试?

Stopfield 2月前

54 0

我正在使用 AutoMapper,我需要在我的一个 Profile 类中注入一项服务。AutoMapper Profile 类并非设计用于支持依赖注入,在这种情况下,记录的模式是……

我正在使用 AutoMapper ,我需要在我的一个 Profile 类中注入一项服务。

AutoMapper Profile 类不是为了支持依赖注入而设计的,在这种情况下应该使用记录的模式 IMappingAction 有关更多详细信息 此处

这些是我需要使用 AutoMapper 映射的类:

public sealed class Student
{
  public int Id { get; set; }
  public string FirstName { get; set; } = string.Empty;
  public string LastName { get; set; } = string.Empty;
  public DateOnly BirthDate { get; set; }
}

public sealed class StudentDto
{
  public string FullName { get; set; } = string.Empty;
  public DateOnly BirthDate { get; set; }
  public string SerialNumber { get; set; } = string.Empty;
}

这是 Profile Student 之间的映射的类 StudentDto

public sealed class StudentProfile : Profile
{
  public StudentProfile()
  {
    CreateMap<Student, StudentDto>()
      .ForMember(
        studentDto => studentDto.FullName,
        options => options.MapFrom(student => $"{student.FirstName} {student.LastName}")
      )
      .ForMember(
        studentDto => studentDto.SerialNumber,
        options => options.Ignore()
      )
      .AfterMap<SetSerialNumberAction>();
  }
}

该类 Profile 引用以下映射操作:

public sealed class SetSerialNumberAction : IMappingAction<Student, StudentDto>
{
  private readonly ISerialNumberProvider _serialNumberProvider;

  public SetSerialNumberAction(ISerialNumberProvider serialNumberProvider)
  {
    _serialNumberProvider = serialNumberProvider ?? throw new ArgumentNullException(nameof(serialNumberProvider));
  }

  public void Process(Student source, StudentDto destination, ResolutionContext context)
  {
    ArgumentNullException.ThrowIfNull(destination);

    destination.SerialNumber = _serialNumberProvider.GetSerialNumber();
  }
}

最后,这是在类中注入的服务 SetSerialNumberAction

public interface ISerialNumberProvider
{
  string GetSerialNumber();
}

public sealed class RandomSerialNumberProvider : ISerialNumberProvider
{
  public string GetSerialNumber() => $"Serial-Number-{Random.Shared.Next()}";
}

由于我使用的是 ASP.NET 核心,因此我依靠 Microsoft DI 来组成我的所有类:

public static class Program
{
  public static void Main(string[] args)
  {
    var builder = WebApplication.CreateBuilder(args);

    // Add services to the container.

    builder.Services.AddControllers();

    builder.Services.AddAutoMapper(typeof(StudentProfile));

    builder.Services.AddSingleton<ISerialNumberProvider, RandomSerialNumberProvider>();

    var app = builder.Build();

    // Configure the HTTP request pipeline.

    app.UseAuthorization();


    app.MapControllers();

    app.Run();
  }
}

有了这种设置,我能够将 IMapper 服务注入到我的控制器类中,并且一切正常。以下是一个例子:

[ApiController]
[Route("api/[controller]")]
public class StudentsController : ControllerBase
{
  private static readonly ImmutableArray<Student> s_students =
  [
    new Student { Id = 1, BirthDate = new DateOnly(1988, 2, 6), FirstName = "Bob", LastName = "Brown" },
    new Student { Id = 2, BirthDate = new DateOnly(1991, 8, 4), FirstName = "Alice", LastName = "Red" },
  ];

  private readonly IMapper _mapper;

  public StudentsController(IMapper mapper)
  {
    _mapper = mapper;
  }

  [HttpGet]
  public IEnumerable<StudentDto> Get()
  {
    return s_students
      .Select(_mapper.Map<StudentDto>)
      .ToArray();
  }
}

问题

上面写的代码运行良好,唯一的问题是在单元测试时。我将要描述的问题的根本原因是我依赖 Microsoft DI 来实例化类 Profile 、映射操作 SetSerialNumberAction 及其依赖项 ISerialNumberProvider .

这是我通常为 AutoMapper 的类编写测试的方式 Profile (此示例用作 XUnit 测试框架):

public sealed class MappingUnitTests
{
  private readonly MapperConfiguration _configuration;
  private readonly IMapper _sut;

  public MappingUnitTests()
  {
    _configuration = new MapperConfiguration(config =>
    {
      config.AddProfile<StudentProfile>();
    });

    _sut = _configuration.CreateMapper();
  }

  [Fact]
  public void Mapper_Configuration_Is_Valid()
  {
    // ASSERT
    _configuration.AssertConfigurationIsValid();
  }

  [Fact]
  public void Property_BirthDate_Is_Assigned_Right_Value()
  {
    // ARRANGE
    var student = new Student
    {
      FirstName = "Mario",
      LastName = "Rossi",
      Id = 1,
      BirthDate = new DateOnly(1980, 1, 5)
    };

    // ACT
    var result = _sut.Map<StudentDto>(student);

    // ASSERT
    Assert.NotNull(result);
    Assert.Equal(student.BirthDate, result.BirthDate);
  }

  [Fact]
  public void Property_FullName_Is_Assigned_Right_Value()
  {
    // ARRANGE
    var student = new Student
    {
      FirstName = "Mario",
      LastName = "Rossi",
      Id = 1,
      BirthDate = new DateOnly(1980, 1, 5)
    };

    // ACT
    var result = _sut.Map<StudentDto>(student);

    // ASSERT
    Assert.NotNull(result);
    Assert.Equal("Mario Rossi", result.FullName);
  }
}

这里有两个不同的问题:

  1. 两个测试 Property_BirthDate_Is_Assigned_Right_Value Property_FullName_Is_Assigned_Right_Value 失败,错误为 System.MissingMethodException:无法动态创建类型为“AutomapperUnitTestingSample.Mapping.SetSerialNumberAction”的实例。原因:未定义无参数构造函数。
  2. 我不知道如何 ISerialNumberProvider 在我的测试中注入模拟实例以便验证属性是否 StudentDto.SerialNumber 通过使用映射操作正确映射 SetSerialNumberAction

可能的解决方案

上述描述的问题可以通过在单元测试中使用 Microsoft DI 来解决。

例如,下面的测试类可以正常工作并解决上面描述的两个问题:

public sealed class MappingUnitTestsWithContainer : IDisposable
{
  private readonly IMapper _sut;
  private readonly ServiceProvider _serviceProvider;
  private readonly Mock<ISerialNumberProvider> _mockSerialNumberProvider;

  public MappingUnitTestsWithContainer()
  {
    // init mock
    _mockSerialNumberProvider = new Mock<ISerialNumberProvider>();

    // configure services
    var services = new ServiceCollection();
    services.AddAutoMapper(typeof(StudentProfile));
    services.AddSingleton<ISerialNumberProvider>(_mockSerialNumberProvider.Object);

    // build service provider
    _serviceProvider = services.BuildServiceProvider();

    // create sut
    _sut = _serviceProvider.GetRequiredService<IMapper>();
  }

  [Fact]
  public void Mapper_Configuration_Is_Valid()
  {
    // ASSERT
    _sut.ConfigurationProvider.AssertConfigurationIsValid();
  }

  [Fact]
  public void Property_BirthDate_Is_Assigned_Right_Value()
  {
    // ARRANGE
    var student = new Student
    {
      FirstName = "Mario",
      LastName = "Rossi",
      Id = 1,
      BirthDate = new DateOnly(1980, 1, 5)
    };

    // ACT
    var result = _sut.Map<StudentDto>(student);

    // ASSERT
    Assert.NotNull(result);
    Assert.Equal(student.BirthDate, result.BirthDate);
  }

  [Fact]
  public void Property_FullName_Is_Assigned_Right_Value()
  {
    // ARRANGE
    var student = new Student
    {
      FirstName = "Mario",
      LastName = "Rossi",
      Id = 1,
      BirthDate = new DateOnly(1980, 1, 5)
    };

    // ACT
    var result = _sut.Map<StudentDto>(student);

    // ASSERT
    Assert.NotNull(result);
    Assert.Equal("Mario Rossi", result.FullName);
  }

  [Fact]
  public void Property_SerialNumber_Is_Set_By_Using_SetSerialNumberAction()
  {
    // ARRANGE
    var student = new Student
    {
      FirstName = "Mario",
      LastName = "Rossi",
      Id = 1,
      BirthDate = new DateOnly(1980, 1, 5)
    };

    // setup mock
    _mockSerialNumberProvider
      .Setup(m => m.GetSerialNumber())
      .Returns("serial-number-123");

    // ACT
    var result = _sut.Map<StudentDto>(student);

    // ASSERT
    Assert.NotNull(result);
    Assert.Equal("serial-number-123", result.SerialNumber);
  }

  public void Dispose()
  {
    _serviceProvider.Dispose();
  }
}

这个问题还有其他解决方案吗? 是否可以通过在单元测试类中不使用 DI 容器来解决此问题?

作为参考,我创建了一个 GitHub 存储库 ,其中包含用于展示此问题的工作代码。

帖子版权声明 1、本帖标题:如何对使用 IMappingAction 的 AutoMapper 配置文件进行单元测试?
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由Stopfield在本站《unit-testing》版块原创发布, 转载请注明出处!
最新回复 (0)
  • 我正在对与 SolidEdge API 交互的方法进行单元测试。Solid Edge API 是一个 COM 组件,用于自动执行 3D 模型更改,如 AutoCAD。模拟数据只能返回一次。之后它会重新...

    交互的单元测试方法 SolidEdge api 。 Solid Edge API 是一个 COM 组件,用于像 AutoCAD 一样自动化 3D 模型更改。

    模拟数据只能返回一次。之后它将返回 null。

    所有模拟都运行良好,但仅在第一次调用时有效。因此,我从 includeIn1 获得空值 includeIn2 。即使两者都访问相同的属性。

    public static ResultModel GetMockedAssemblyDocument(MockRequestModel mockRequestModel)
    {
        var mockAssemblyDocument = new Mock<AssemblyDocument>();
        var mockOccurrences = new Mock<Occurrences>();
        var mockOccurrenceList = new List<Mock<Occurrence>>();
    
        foreach (var occurrence in mockRequestModel.OccurrencesDetails)
        {
            var mockOccurrence = MockSolidEdgeDocumentOccurrence(occurrence.OccurrenceName, occurrence.Properties, occurrence.Variables);
            mockOccurrenceList.Add(mockOccurrence);
        }
    
        mockOccurrences.Setup(o => o.GetEnumerator()).Returns(mockOccurrenceList.Select(mock => mock.Object).GetEnumerator());
        mockAssemblyDocument.Setup(doc => doc.Occurrences).Returns(mockOccurrences.Object);
    
        return new ResultModel { MockedAssemblyDocument = mockAssemblyDocument, MockedAssemblyDocumentCount = 2 };
    }
    
    public static Mock<Occurrence> MockSolidEdgeDocumentOccurrence(string occurrenceName, Dictionary<string, object> properties,
        Dictionary<string, VariableInfo> variables)
    {
       // Existing code
        mockOccurrence1.SetupGet(o => o.OccurrenceDocument).Returns(() =>
        {
            var mockSolidEdgeDocument = new Mock<SolidEdgeDocument>();
    
            var mockPropertySets = GetPropertySetsMock(properties);
            mockSolidEdgeDocument.Setup(doc => doc.Properties).Returns(mockPropertySets.Object);
    
            return mockSolidEdgeDocument.Object;
        });
    
        return mockOccurrence1;
    }
    
    private static Mock<PropertySets> GetPropertySetsMock(Dictionary<string, object> properties)
    {
        var mockPropertySets = new Mock<PropertySets>();
        var mockPropertySet = new Mock<Properties>();
    
        var propertyObjectType = new List<dynamic>();
        foreach (var property in properties)
        {
            dynamic dynamicProperty = new ExpandoObject();
            dynamicProperty.Name = property.Key;
            dynamicProperty.Value = property.Value;
    
            propertyObjectType.Add(dynamicProperty);
        }
        mockPropertySet.Setup(ps => ps.GetEnumerator()).Returns(propertyObjectType.GetEnumerator());
        mockPropertySets.Setup(ps => ps.GetEnumerator()).Returns(new List<Properties> { mockPropertySet.Object }.GetEnumerator());
    
        return mockPropertySets;
    }
    
    [Test]
    public void GetSupportData_ShouldReturnListOfComponents()
    {
        // Arrange
        var mockRequestModel = new MockRequestModel
        {
            OccurrencesDetails = new List<MockOccurrenceRequestModel>
            {
                new()
                {
                    OccurrenceName = "Occurrence1",                  
                    Properties = new  Dictionary<string, object>()
                    {
                        { nameof(FilePropertyNameConstants.ObjectType), nameof(ObjectTypeConstants.Elbow) },
                        { nameof(FilePropertyNameConstants.IncludeIn), "Yes" }
                    }
                }
                
            }
        };
    
        var mockAssemblyDocument = GetMockedAssemblyDocument(mockRequestModel);
        _mockConnectionRepository.Setup(repo => repo.GetAssemblyDocument()).Returns(mockAssemblyDocument.MockedAssemblyDocument.Object);
    
        // Act
        var result = _isometricRepository.GetData();
    
        // Assert
        Assert.That(result, Is.Not.Null);      
    }
    
    public List<ResultModel> GetData()
    {
        // Existing code 
        
            SolidEdgeDocument occurrenceDocument = occurrence.OccurrenceDocument;
    
            if (!occurrenceDocument.PropertyValueExists(FilePropertyNameConstants.IncludeIn, out object? includeIn1)) continue;
            if (!occurrenceDocument.PropertyValueExists(FilePropertyNameConstants.IncludeIn, out object? includeIn2)) continue;
    
            // Existing code 
        
    }
    
  • 在我看来,我们在项目中使用的任何库都应该简化代码编写,使编写代码变得更容易。而 AutoMapper 则是一个庞然大物,有大量的文档,这使得编写代码(例如将一种类型映射到另一种类型)变得非常复杂。

  • @MohammadAghazadeh 我并不是想测试库内部,而是想测试提供给库本身的映射配置。

  • @AlexanderPetrov 我同意你关于 AutoMapper 复杂性的看法。我并不是 AutoMapper 的专家,但我在一个大型团队中工作,我们的一些代码库大量使用了它。但我明白你的意思,我基本同意。

  • 这个问题还有其他的解决方案吗?

    是的。只需不要在 AutoMapper 中使用 DI。

    但是...我想在 AutoMapper 中使用 DI。

    那么抱歉,但你使用 AutoMapper 的方式是错误的。也许你现在没有看到它,但如果你在接下来的项目中继续使用 AutoMapper,你将来会看到它。

    正如 AutoMapper 的作者 Jimmy Bogard 在关于 AutoMapper 的设计理念的 :

    AutoMapper 之所以有效,是因为它强制执行了一种约定。它假设您的目标类型是源类型的子集。它假设您的目标类型上的所有内容都应进行映射。它假设目标成员名称遵循源类型的确切名称。它假设您希望将复杂模型扁平化为简单模型。

    对我来说,它还假设您要映射的内容很简单,但无处不在,因此您不想手动执行该过程,因为需要编写太多无聊的代码。充分利用 AutoMapper 并避免产生令人讨厌的副作用的关键是保持简单。知道何时使用它,何时不使用,并认识到您应该选择可单元测试、定制、手动方法而不是 AutoMapper 的时刻。

    我知道 AutoMapper 本身支持 DI,但在我看来,当你需要 DI 来映射某些模型时,恐怕它就不再简单了。请记住,它应该是简单的,让你的项目处于比使用该库之前更好的状态。保持简单可以让你的代码库可维护性保持在较低水平。

    另一方面,让自定义解析器、DI、后映射或前映射在您的配置文件周围疯狂增长,会给您或在您之后试图扩展或修复您留下的代码的程序员带来麻烦。将您的业务逻辑(是的,设置序列号是业务逻辑)放入您的映射配置文件中是一个错误,它会影响代码的可维护性。您已经经历过这种情况,因为您已经注意到测试这种映射配置有多么困难。现在想象一下,试图在一个一半业务逻辑分散在映射配置文件周围的项目中寻找一些错误。我见过这样的案例,相信我,这是一场噩梦。StackOverflow 本身就充满了这样的代码问题。

    另外,请查看 Jimmy 的 AutoMapper 使用指南 。值得一读。

    测试

    AutoMapper 具有与其他映射库不同的独特功能,即 配置验证 集成测试 ,则效果最佳 。使用简单的映射配置文件,这就是您所需要的一切。AutoMapper 将自动检测何时有人添加了新属性,而现在缺少该属性的映射。与 CI 配合使用,这将使您对模型具有很高的控制力。

    public class AutoMapperTests : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> _factory;
    
        public AutoMapperTests(WebApplicationFactory<Program> factory)
        {
            _factory = factory;
        }
    
        [Fact]
        public void Should_Not_Throw_WithValidConfiguration()
        {
            // Assert
            // Get mapper from service provider.
            // DO NOT create it manually if your mapping profiles use DI.
        
            // Act
            var exception = Record.Exception(() => mapper.ConfigurationProvider.AssertConfigurationIsValid());
    
            // Assert
            Assert.Null(exception);
        }
    }
    

    该怎么办?

    所以,这确实是一个很长的话题...但是,在你的场景中你应该采取什么实际步骤呢?

    1. p12

    2. p13

返回
作者最近主题: