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

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

Stopfield 2月前

56 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)
  • 在我看来,我们在项目中使用的任何库都应该简化代码编写,使编写代码变得更容易。而 AutoMapper 则是一个庞然大物,有大量的文档,这使得编写代码(例如将一种类型映射到另一种类型)变得非常复杂。

返回
作者最近主题: