0

We have an API with about a dozen integration tests. All the tests passed until I added some DTOs and used AutoMapper. Now, all the tests that test methods that use AutoMapper and the DTOs are failing. I have provided all the code needed to understand one of the failing tests. Also, I read a lot about AutoMapper and the following StackOverflow posts:

  1. Integration Testing with AutoMapper fails to initialise configuration
  2. A kind of integration testing in ASP.NET Core, with EF and AutoMapper

Startup.cs

This is our Startup.ConfigureServices(). I have tried every code block commented out and/or marked "ATTEMPTED".

 public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddDbContext<OurContext>(options =>
                options.UseSqlServer(Configuration["ConnectionString"]))
            .AddDbContext<OurContext>()
            .AddRazorPages()
            .AddMvcOptions(options => options.EnableEndpointRouting = false)
            .AddNewtonsoftJson(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
        services
            .AddControllersWithViews();

        //ATTEMPTED
        //services
        //    .AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

        //ATTEMPTED
        //MapperConfiguration mapperConfiguration = new MapperConfiguration(mc =>
        //{
        //    mc.AddProfile(new OurProfile());
        //});
        //IMapper mapper = mapperConfiguration.CreateMapper();
        //services
        //    .AddSingleton(mapper);

        //ATTEMPTED
        //services
        //    .AddAutoMapper(typeof(Startup));

        //ATTEMPTED
        //var assembly = typeof(Program).GetTypeInfo().Assembly;
        //services
        //    .AddAutoMapper(assembly);

        //ATTEMPTED
        var assembly = typeof(Program).GetTypeInfo().Assembly;
        services.AddAutoMapper(cfg =>
        {
            cfg.AllowNullDestinationValues = true;
            cfg.CreateMap<OurModel, OurDto>()
                .IgnoreAllPropertiesWithAnInaccessibleSetter();
        }, assembly);
    }

Controller

This is our controller.

[Route("api/[controller]")]
[ApiController]
public class OurController : ControllerBase
{
    private readonly OurContext _context;

    protected readonly ILogger<OurController> Logger;

    private readonly IMapper _mapper;

    public OurController(OurContext context, ILogger<OurController> logger, 
        IMapper mapper)
    {
        _context = context ??
            throw new ArgumentNullException(nameof(context));
        Logger = logger ??
                 throw new ArgumentNullException(nameof(logger));
        _mapper = mapper ??
                  throw new ArgumentNullException(nameof(mapper));
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
    {

        IQueryable<OurModel> models = _context.OurModel;

        IQueryable<OurDto> dtos = 
            _mapper.Map<IQueryable<OurDto>>(models);

        return await dtos.ToListAsync();
    }
}

Profile, Model, and DTO

Profile

public class OurProfile : Profile
{
    public OurProfile()
    {
        CreateMap<OurModel, OurDto>();
    }
}

Model

public partial class OurModel
{
    public string Number { get; set; }
    public string Name1 { get; set; }
    public string Name2 { get; set; }
    public string Status { get; set; }
    public DateTime? Date { get; set; }
    public string Description { get; set; }
    public string Comment { get; set; }
    public string District { get; set; }
}

DTO

public class OurDto
{
    public string Number { get; set; }
    public string Name1 { get; set; }
    public string Name2 { get; set; }
    public string Status { get; set; }
    public DateTime? Date { get; set; }
    public string Description { get; set; }
    public string Comment { get; set; }
    public string District { get; set; }
}

Test Fixture

This is our test fixture.

public abstract class ApiClientFixture : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> _factory;

    protected abstract string RelativeUrl { get; }

    protected ApiClientFixture(WebApplicationFactory<Startup> factory)
    {
        _factory = factory;
    }

    protected HttpClient CreateClient()
    {
        HttpClient client;
        var builder = new UriBuilder();

        client = _factory.CreateClient();
        builder.Host = client.BaseAddress.Host;
        builder.Path = $"{RelativeUrl}";

        client.BaseAddress = builder.Uri;
        return client;
    }
}

Test

This is our test class. The single test in this test class fails.

public class Tests : ApiClientFixture
{
    public Tests(WebApplicationFactory<Startup> factory) : base(factory)
    {
    }

    protected override string RelativeUrl => "api/OurController/";

    [Fact]
    public async void GetAllReturnsSomething()
    {
        var response = await CreateClient().GetAsync("");

        Assert.True(response.IsSuccessStatusCode);
    }
}

When I debug the test I see that a 500 status code is returned from the URL provided to the in-memory API.

Does anybody have some suggestions? More than half of our tests currently fail, and I suspect that AutoMapper is not configured properly for integration testing.

Thor
  • 181
  • 1
  • 2
  • 8
  • Few thoughts: AutoMapper does not map `IQuerable` by default and I don't see in your code that you create a map for it. Integration tests usually launch from a different assembly, so ensure you are adding all required mapping profiles properly (with a debugger for example). You can resolve `IConfigurationProvider` and inspect it for profiles. And what does the error say besides the fact that it returns 500 status code? Set a breakpoint in controller, see if it is reaching that point. Step by step, inspect what is happening. Try to reach the point where code is failing. – Prolog Aug 20 '20 at 20:55
  • @Prolog Thanks for your thoughts. Your first comment directly addressed the problem, and I'll post an answer to this question. Also, I need to learn when the debugger is helpful. When using Postman I have used the debugger, but I did not think to put breakpoints in the controller to debug the integration tests. – Thor Aug 21 '20 at 16:02

2 Answers2

2

Creating a map for IQueryable<T> is not really a good solution. In your answer you are losing proper flow of asynchronous database querying. I wrote about IQueryable<T> in a comment because you were looking for a 500 error cause. Making it work it's a one thing, making it a good solution it's another thing, however.

I'd strongly suggest to use AutoMapper ProjectTo() extension which you can use directly on a IQueryable<T> sequence. It let's you combine mapping and querying in one go. More or less it does a Select() based on your mappings, so it not only gives you proper model right away with the query result, but it also reduces the amount of columns obtained from database, which can make the query run faster. But, there are of course limitations to it, e.g. you can't use custom type converters or conditional mapping. You can read more about Project() in the documentation.

Usage:

public async Task<ActionResult<List<OurDto>>> GetAll()
{
    return await _context
        .OurModel
        .ProjectTo<OutDto>(_mapper.ConfigurationProvider)
        .ToListAsync();
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Prolog
  • 2,698
  • 1
  • 18
  • 31
0

Thanks to @Prolog for his comment. I realized that I need to map each element of the IQueryable individually, so I rewrote my Controller method.

Also, side note: IList.AsQueryable().ToListAsync() does not work, so I wrote:

IQueryable<OurDto> dtosQueryable = dtos.AsQueryable();
return await Task.FromResult(dtosQueryable.ToList());

Old Controller Method

[HttpGet]
public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
{

    IQueryable<OurModel> models = _context.OurModel;

    IQueryable<OurDto> dtos = 
        _mapper.Map<IQueryable<OurDto>>(models);

    return await dtos.ToListAsync();
}

New Controller Method

    public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
    {
        IQueryable<OurModel> models = _context.OurModel;

        IList<OurDto> dtos = new List<OurDto>();
        foreach (OurModel model in models)
        {
            OurDto dto = _mapper.Map<OurDto>(model);
            dtos.Add(dto);
        }
        IQueryable<OurDto> dtosQueryable = dtos.AsQueryable();

        return await Task.FromResult(dtosQueryable.ToList());
    }
Thor
  • 181
  • 1
  • 2
  • 8