0

I'm writing a unit test for ConsumerService that uses EntityFramework Core. Below you can see my AppDBContext class, and its one and only constructor. In order to fake it, I'm required to pass DbContext into it, so, I'm trying to fake that one too. I'm having an issue faking the DbContext object.

Here is my AppDBContext class:

   public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

**Consumer **class then wires it in like this:

        private readonly AppDbContext _db;

        public ConsumerService(AppDbContext db)
        {
            _db = db;
        }

I'm trying to write a unit test for the consumer class.

I've tried this:

        private readonly DbContextOptions<AppDbContext> _options;
        private readonly AppDbContext _db;
        private readonly ConsumerService _consumerService ;

        public ConsumerServiceTests()
        {
            //Dependencies
            _options = A.Fake<DbContextOptions<AppDbContext>>();
            _db = A.Fake<AppDbContext>(x => x.WithArgumentsForConstructor(() => new AppDbContext(_options)));

            //SUT
            _consumerService = new ConsumerService(_db);
        }

I'm getting this error:

Message:  FakeItEasy.Core.FakeCreationException : Failed to create fake of type API.Data.AppDbContext: No constructor matches the passed arguments for constructor. An exception of type System.InvalidOperationException was caught during this call. Its message was: The DbContextOptions passed to the AppDbContextProxy constructor must be a DbContextOptions. When registering multiple DbContext types, make sure that the constructor for each context type has a DbContextOptions parameter rather than a non-generic DbContextOptions parameter. at Microsoft.EntityFrameworkCore.DbContext..ctor(DbContextOptions options) at API.Data.AppDbContext..ctor(DbContextOptions1 options) in C:\Users\...\API\Data\AppDbContext.cs:line 8 at Castle.Proxies.AppDbContextProxy..ctor(IInterceptor[], DbContextOptions1 options) at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.ConstructorInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

NOTE

  1. I use FakeItEasy.
  2. No, I don't want to use InMemoryDatabase. I need to fake DbContext.
  • If you want to test make a copy of the database and connect to copy. – jdweng Apr 06 '23 at 16:54
  • @jdweng Please read the Note section –  Apr 06 '23 at 16:54
  • 1
    _"I need to fake DbContext."_ - But why? Sound like [XY-problem](https://en.m.wikipedia.org/wiki/XY_problem) to me. – Guru Stron Apr 06 '23 at 16:54
  • @GuruStron AppDbContext has it as a dependency. And ConsumerService has AppDbContext as dependency. And it is the only constructor that AppDbContext has. So I need to fake it, in order to pass it then to fake AppDbContext –  Apr 06 '23 at 16:55
  • 1
    Just new up an instance of it - `new DbContextOptions()`. There is absolutely no point in faking it. – Guru Stron Apr 06 '23 at 16:59
  • I read it. Making a copy isn't a InMemoryDatabase. It is an new instance of the database. – jdweng Apr 06 '23 at 17:01
  • @jdweng Why would I do that for a Unit Test? –  Apr 06 '23 at 17:03
  • @GuruStron I'm not sure what happened, but it worked. You can't imagine how many combinations I tried. I mean I was sure I already done that. Thanks –  Apr 06 '23 at 17:05
  • You do not want to modify an existing database with vital info. – jdweng Apr 06 '23 at 20:54
  • @fredward this is because the members of the fake DbContextOptions returns dummy values unless configured. So `_options.ContextType` returns a fake type, instead of `AppDbContext`. So [this check](https://github.com/dotnet/efcore/blob/0c5ae3bed7cc1323df3b6fd29d7f95f2cb406d36/src/EFCore/DbContext.cs#L111) fails in the DbContext constructor. Using a real DbContextOptions returns the expected type, so it works. – Thomas Levesque Apr 07 '23 at 01:27
  • Could you elaborate why you are trying to mock concrete classes? Usually, IoC means that you should replace concrete implementations with interfaces or abstract classes and mock them instead. – Evgeny Shmanev Apr 07 '23 at 07:46
  • @EvgenyShmanev I needed to pass it to the AppDbContext constructor. I thought It was necessary to mock, but then Guru Stron explained that i can just create a new instance instead of Faking it. –  Apr 07 '23 at 12:03
  • Correct. When you work with concrete classes, you should not mock them. When there is dependency of an interface, the interface should be mocked. Btw, I would rather introduce a new interface IAppDbContext and pass it to the consumer service. – Evgeny Shmanev Apr 08 '23 at 13:42

2 Answers2

0

I'm usually use InMemoryDatabase for testing my services. By the way you should Instantiate a DbContextOptionsBuilder:

 var options = new DbContextOptionsBuilder<Context>().Options;

It gives you what you wanted.

RezaNoei
  • 1,266
  • 1
  • 8
  • 24
0

You can create an abstract class and then use it in other classes by inheriting it, like:

public abstract class BaseDbContext
{
  protected AppDbContext _db;
  
  public BaseDbContext()
  {
    var builder = new DbContextOptionsBuilder<AppDbContext>();
    builder.UseSqlite("DataSource:=memory:", _ => {});
    _db = new AppDbContext(builder.Options);
    _db.Database.OpenConnection();
    _db.Database.EnsureCreated();
  }
}
Elyas Esna
  • 625
  • 4
  • 19