Классы профиля AutoMapper не предназначены для поддержки зависимостей внедрение, в данном случае документированный шаблон должен использовать 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;
}
Код: Выделить всё
public sealed class StudentProfile : Profile
{
public StudentProfile()
{
CreateMap()
.ForMember(
studentDto => studentDto.FullName,
options => options.MapFrom(student => $"{student.FirstName} {student.LastName}")
)
.ForMember(
studentDto => studentDto.SerialNumber,
options => options.Ignore()
)
.AfterMap();
}
}
Код: Выделить всё
public sealed class SetSerialNumberAction : IMappingAction
{
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();
}
}
Код: Выделить всё
public interface ISerialNumberProvider
{
string GetSerialNumber();
}
public sealed class RandomSerialNumberProvider : ISerialNumberProvider
{
public string GetSerialNumber() => $"Serial-Number-{Random.Shared.Next()}";
}
Код: Выделить всё
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();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
Код: Выделить всё
[ApiController]
[Route("api/[controller]")]
public class StudentsController : ControllerBase
{
private static readonly ImmutableArray 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 Get()
{
return s_students
.Select(_mapper.Map)
.ToArray();
}
}
Код, написанный выше, работает нормально, единственная проблема заключается в модульном тестировании.
Основная причина проблема, которую я собираюсь описать, заключается в том, что я полагаюсь на Microsoft DI для создания экземпляра класса Profile, действия сопоставления SetSerialNumberAction и его зависимости ISerialNumberProvider.
Вот как я обычно пишу тесты для классов профиля AutoMapper (в этом примере в качестве среды тестирования используется XUnit):
Код: Выделить всё
public sealed class MappingUnitTests
{
private readonly MapperConfiguration _configuration;
private readonly IMapper _sut;
public MappingUnitTests()
{
_configuration = new MapperConfiguration(config =>
{
config.AddProfile();
});
_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(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(student);
// ASSERT
Assert.NotNull(result);
Assert.Equal("Mario Rossi", result.FullName);
}
}
- оба теста Property_BirthDate_Is_Assigned_Right_Value и Property_FullName_Is_Assigned_Right_Value завершаются неудачей с ошибкой System.MissingMethodException: невозможно динамически создать экземпляр типа AutomapperUnitTestingSample.Mapping.SetSerialNumberAction. Причина: не определен конструктор без параметров.
- Я не знаю, как внедрить фиктивный экземпляр ISerialNumberProvider в свои тесты, чтобы проверить, что свойство StudentDto .SerialNumber правильно сопоставляется с помощью действия сопоставления SetSerialNumberAction.
Описанную выше проблему можно решить с помощью Microsoft DI в модульном тесте.
Например, следующий тестовый класс работает нормально и решает обе описанные выше проблемы:
Код: Выделить всё
public sealed class MappingUnitTestsWithContainer : IDisposable
{
private readonly IMapper _sut;
private readonly ServiceProvider _serviceProvider;
private readonly Mock _mockSerialNumberProvider;
public MappingUnitTestsWithContainer()
{
// init mock
_mockSerialNumberProvider = new Mock();
// configure services
var services = new ServiceCollection();
services.AddAutoMapper(typeof(StudentProfile));
services.AddSingleton(_mockSerialNumberProvider.Object);
// build service provider
_serviceProvider = services.BuildServiceProvider();
// create sut
_sut = _serviceProvider.GetRequiredService();
}
[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(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(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(student);
// ASSERT
Assert.NotNull(result);
Assert.Equal("serial-number-123", result.SerialNumber);
}
public void Dispose()
{
_serviceProvider.Dispose();
}
}
В качестве справки я создал репозиторий GitHub, содержащий рабочий код для демонстрации этой проблемы.< /п>
Подробнее здесь: https://stackoverflow.com/questions/785 ... pingaction
Мобильная версия