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

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

Stopfield 2月前

55 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 
        
    }
    
返回
作者最近主题: