Как я могу протестировать профиль AutoMapper, который использует IMappingAction?C#

Место общения программистов C#
Ответить
Anonymous
 Как я могу протестировать профиль AutoMapper, который использует IMappingAction?

Сообщение Anonymous »

Я использую AutoMapper, и мне нужно внедрить службу в один из классов профиля.
Классы профиля 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;
}
Это класс Profile, в котором настраивается сопоставление между Student и StudentDto:

Код: Выделить всё

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();
}
}
Класс Profile ссылается на следующее действие сопоставления:

Код: Выделить всё

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();
}
}
Наконец, это служба, внедряемая в класс 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();

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 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);
}
}
Здесь есть 2 разные проблемы:
  • оба теста 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();
}
}
Есть ли другое решение этой проблемы? Можно ли решить эту проблему, не используя DI-контейнер в классе модульного теста?
В качестве справки я создал репозиторий GitHub, содержащий рабочий код для демонстрации этой проблемы.< /п>

Подробнее здесь: https://stackoverflow.com/questions/785 ... pingaction
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»