Search code examples
asp.net-coreasp.net-web-apimockingxunit

How to Perform Testing with xUnit and Mock with an In-Memory Database?


In ASP.NET Core 6, I'm using MariaDB and have a Controller that injects IService via dependency injection in its constructor. The Service injects its IRepository, which inherits from a GenericRepository that contains the DBContext injection. I want to use an in-memory database during testing, so I created DataBaseFixture : IDisposable, which also populates some data. However, I'm having difficulty implementing the tests and specifying that the context from DataBaseFixture should be used.

ArtistaController.cs

[Route("api/[controller]")]
public class ArtistaController : ControllerBase
{
    private readonly IArtistaService _artistaService;

    public ArtistaController(IArtistaService context)
    {
        _artistaService = context;
    }
    ...
}

ArtistaService.cs

public class ArtistaService : IArtistaService
{
    private readonly IArtistaRepository _artistaRepository;
    private readonly IContextRepository _contextRepository;

    public ArtistaService(IArtistaRepository artistaRepository, IContextRepository contextRepository)
    {
        _artistaRepository = artistaRepository;
        _contextRepository = contextRepository;
    }
    ...
}

ArtistaRepository.cs

public class ArtistaRepository : GenericRepository<ArtistaModel>, IArtistaRepository
{
    public ArtistaRepository(OrientoonContext context) : base(context)
    {
    }
}

GenericRepository.cs

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    protected readonly OrientoonContext _context;
    protected readonly DbSet<T> _dbSet;

    public GenericRepository(OrientoonContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }
    ...
}

DataBaseFixture.cs

public class DataBaseFixture : IDisposable
{
    public OrientoonContext Context { get; private set; }

    public DataBaseFixture()
    {
        var options = new DbContextOptionsBuilder<OrientoonContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        
        Context = new OrientoonContext(options);

        SeedDatabase();
    }

    private void SeedDatabase()
    {
        Context.Artista.AddRange(
            new ArtistaModel { Id = "1", nome = "Oda" },
            new ArtistaModel { Id = "2", nome = "Echiro" },
            new ArtistaModel { Id = "3", nome = "Echiro Oda" }
        );
        Context.SaveChanges();
    }

    public void Dispose()
    {
        Context.Database.EnsureDeleted();
        Context.Dispose();
    }
}

The result is always returning null, and I've tried various approaches.

ArtistaControllerTest.cs

public class ArtistaControllerTest : IClassFixture<DataBaseFixture>
{
    private readonly DataBaseFixture _fixture;

    private readonly Mock<IArtistaService> _artistaServiceMock;
    private readonly ArtistaController _controller;

    public ArtistaControllerTest(DataBaseFixture fixture)
    {
        _fixture = fixture;
        _artistaServiceMock = new Mock<IArtistaService>();
        _artistaServiceMock.Setup(service => service.GetAsync(It.IsAny<string>())).ReturnsAsync((string id) =>
        {
            var artista = _fixture.Context.Artista.FirstOrDefault(a => a.Id == id);
            return new ArtistaForm { Id = artista.Id, Nome = artista.nome };
        });
        _controller = new ArtistaController(_artistaServiceMock.Object);
    }

    [Fact]
    public async void GetArtista()
    {
        var artistaId = "1";
        var expectedArtista = new ArtistaModel { Id = artistaId, nome = "Oda" };

        var result = await _controller.Get(artistaId);

        // Assert
        var okResult = Assert.IsType<OkObjectResult>(result.Result);
        var returnValue = Assert.IsType<ArtistaForm>(okResult.Value);
        Assert.Equal(expectedArtista.Id, returnValue.Id);
        Assert.Equal(expectedArtista.nome, returnValue.Nome);
    }
}

I tried setting up unit tests for my ASP.NET Core application using xUnit and Moq with an in-memory database. I expected the tests to correctly use the seeded data from the in-memory database and return the expected results. However, the results were always null. Specifically, I wanted the ArtistaController to retrieve an ArtistaModel object based on its ID, but despite the setup, the test did not return the expected data from the in-memory database.


Solution

  • Here is a whole working demo you could follow:

    Services

    public interface IArtistaService
     {
         Task<ArtistaForm> GetAsync(string id);
     }
    
     public interface IArtistaRepository : IGenericRepository<ArtistaModel>
     {
         Task<ArtistaModel> GetByIdAsync(string id);
     }
    
     public interface IGenericRepository<T> where T : class
     {
         Task AddAsync(T entity);
         Task DeleteAsync(T entity);
         Task UpdateAsync(T entity);
         Task<IEnumerable<T>> GetAllAsync();
         Task<T> GetByIdAsync(string id);
     }
     public class ArtistaService : IArtistaService
     {
         private readonly IArtistaRepository _artistaRepository;
    
         public ArtistaService(IArtistaRepository artistaRepository)
         {
             _artistaRepository = artistaRepository;
         }
    
         public async Task<ArtistaForm> GetAsync(string id)
         {
             var artista = await _artistaRepository.GetByIdAsync(id);
             if (artista == null) return null;
             return new ArtistaForm { Id = artista.Id, Nome = artista.nome };
         }
    
         
     }
     public class ArtistaRepository : GenericRepository<ArtistaModel>, IArtistaRepository
     {
         public ArtistaRepository(MvcProj8_0Context context) : base(context)
         {
         }
    
         public async Task<ArtistaModel> GetByIdAsync(string id)
         {
             return await _dbSet.FindAsync(id);
         }
    
        
     }
     public class GenericRepository<T> : IGenericRepository<T> where T : class
     {
         protected readonly MvcProj8_0Context _context;
         protected readonly DbSet<T> _dbSet;
    
         public GenericRepository(MvcProj8_0Context context)
         {
             _context = context;
             _dbSet = context.Set<T>();
         }
    
         public async Task AddAsync(T entity)
         {
             await _dbSet.AddAsync(entity);
             await _context.SaveChangesAsync();
         }
    
         public async Task DeleteAsync(T entity)
         {
             _dbSet.Remove(entity);
             await _context.SaveChangesAsync();
         }
    
         public async Task UpdateAsync(T entity)
         {
             _dbSet.Update(entity);
             await _context.SaveChangesAsync();
         }
    
         public async Task<IEnumerable<T>> GetAllAsync()
         {
             return await _dbSet.ToListAsync();
         }
    
         public async Task<T> GetByIdAsync(string id)
         {
             return await _dbSet.FindAsync(id);
         }
     }
    

    Controller

        public class ArtistaController : ControllerBase
    {
        private readonly IArtistaService _artistaService;
    
        public ArtistaController(IArtistaService artistaService)
        {
            _artistaService = artistaService;
        }
    
        [HttpGet("{id}")]
        public async Task<ActionResult<ArtistaModel>> Get(string id)
        {
            var artista = await _artistaService.GetAsync(id);
            if (artista == null)
            {
                return NotFound();
            }
            return Ok(artista);
        }
    
    }
    

    DataBaseFixture

    public class DataBaseFixture : IDisposable
    {
        public MvcProj8_0Context Context { get; private set; }
    
        public DataBaseFixture()
        {
            var options = new DbContextOptionsBuilder<MvcProj8_0Context>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString())
                
                .Options;
    
            Context = new MvcProj8_0Context(options);
            
            SeedDatabase();
        }
    
        private void SeedDatabase()
        {
            Context.Artista.AddRange(
                new ArtistaModel { Id = "1", nome = "Oda" },
                new ArtistaModel { Id = "2", nome = "Echiro" },
                new ArtistaModel { Id = "3", nome = "Echiro Oda" }
            );
            Context.SaveChanges();
        }
    
        public void Dispose()
        {
            Context.Database.EnsureDeleted();
            Context.Dispose();
        }
    }
    

    Test

    public class ArtistaControllerTest : IClassFixture<DataBaseFixture>
      {
          private readonly DataBaseFixture _fixture;
          private readonly ArtistaController _controller;
    
          public ArtistaControllerTest(DataBaseFixture fixture)
          {
              _fixture = fixture;
    
              // Setup the repository and service using the in-memory context
              var artistaRepository = new ArtistaRepository(_fixture.Context);
              var artistaService = new ArtistaService(artistaRepository);
    
              // Initialize the controller with the actual service
              _controller = new ArtistaController(artistaService);
          }
    
          [Fact]
          public async Task GetArtista()
          {
              var artistaId = "1";
              var expectedArtista = new ArtistaForm { Id = artistaId, Nome = "Oda" };
    
              var result = await _controller.Get(artistaId);
    
              // Assert
              var okResult = Assert.IsType<OkObjectResult>(result.Result);
              var returnValue = Assert.IsType<ArtistaForm>(okResult.Value);
              Assert.Equal(expectedArtista.Id, returnValue.Id);
              Assert.Equal(expectedArtista.Nome, returnValue.Nome);
          }
    
        
      }