The Sexiest way to Inject dbcontext in dotnetcore for write test in Test Layer
I wrote a apllication in this application i created a service layer that inject dbcontext in constructore of service class and use these service interface in the presentation layer and have an other layer for infrastruture and write dbcontect and migration in this layer after that for wrote my unit test or integration test or business behavour test, I create a new layer and named it to test layer but i can some problems because for use service i need to inject my dbcontext to all the services and other problem was I don’t want to use real database and want to create a sample database and used this data base for test them destroy that after finish test, for solving this problem you can follow this article because i can find a very easy and usable way to wrote test. You can use this way for writing TDD or BDD development this is practical way and used this way for my projects.
Lets start…
Install InMemoryDatabase
In order to use InMemory database, we need to install a NuGet package, i.e., Microsoft.EntityFrameworkCore.InMemory.
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.21" />
Create a model for Customer. This model will be having fields like Id and Name and etc. Please make sure that the Id column is added in each model.
public class Customer:BaseEntity<Guid>
{
public Customer(PhoneNumber PhoneNumber, Email Email, BankAccountNumber BankAccountNumber, CustomerInfo CustomerInfo)
{
this.Id = Guid.NewGuid();
this.PhoneNumber = PhoneNumber;
this.Email = Email;
this.BankAccountNumber = BankAccountNumber;
this.CustomerInfo = CustomerInfo;
}
public Customer()
{
}
public PhoneNumber PhoneNumber { get; private set; }
public Email Email { get; private set; }
public BankAccountNumber BankAccountNumber { get; private set; }
public CustomerInfo CustomerInfo { get; private set; }
public CustomerInfo UpdateInfo(CustomerInfo customerInfo)
{
return this.CustomerInfo = customerInfo;
}
public PhoneNumber SetPhoneNumber(PhoneNumber phoneNumber)
{
return this.PhoneNumber = phoneNumber;
}
public Email SetEmail(Email email)
{
return this.Email = email;
}
public BankAccountNumber SetBankAccountNumber(BankAccountNumber bankAccountNumber)
{
return this.BankAccountNumber = bankAccountNumber;
}
}
Create a class called CustomerDbContext which inherits from DbContext.
Declare the public property of type DbSet for Customer.
From the parameterized constructor, you can load categories. This data can be loaded from the database, but for demo purposes, I have added hardcoded ones.
public class CustomerDbContext : DbContext
{
public CustomerDbContext(DbContextOptions<CustomerDbContext> options)
: base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>().OwnsOne(p => p.CustomerInfo).Property(x=>x.FirstName).HasColumnType("varchar(50)");
modelBuilder.Entity<Customer>().OwnsOne(p => p.CustomerInfo).Property(x => x.LastName).HasColumnType("varchar(50)");
modelBuilder.Entity<Customer>().OwnsOne(p => p.Email).Property(x => x.email).HasColumnType("varchar(50)");
modelBuilder.Entity<Customer>().OwnsOne(p => p.BankAccountNumber).Property(x => x.BankAccount).HasColumnType("varchar(50)");
modelBuilder.Entity<Customer>().OwnsOne(p => p.PhoneNumber).Property(x => x.Number).HasColumnType("varchar(13)");
}
public DbSet<Customer> Customers { get; set; }
}
Then I implement Service layer like this.
public class CustomerService : ICustomerService
{
public CustomerService(CustomerDbContext _dbContext)
{
DbContext = _dbContext;
}
public CustomerDbContext DbContext { get; }
public async Task<Customer> AddAsync(Customer entity)
{
var customer = new Customer(entity.PhoneNumber, entity.Email, entity.BankAccountNumber, entity.CustomerInfo);
var findcustomer= DbContext.Customers
.Where(a=>a.CustomerInfo.FirstName==entity.CustomerInfo.FirstName &&
a.CustomerInfo.LastName == entity.CustomerInfo.LastName &&
a.CustomerInfo.DateOfBirth == entity.CustomerInfo.DateOfBirth).FirstOrDefault();
if (findcustomer != null)
{
throw new Exception("Customers must be unique in the database: By Firstname, Lastname, and DateOfBirth");
}
var findcustomerwithemail = DbContext.Customers
.Where(a => a.Email.email == entity.Email.email).FirstOrDefault();
if (findcustomerwithemail != null)
{
throw new Exception("Email must be unique in the database.");
}
await DbContext.Customers.AddAsync(customer);
await DbContext.SaveChangesAsync();
return customer;
}
public async Task<bool> DeleteAsync(Customer entity)
{
var customer = DbContext.Customers.Remove(entity);
return customer!=null?true:false;
}
public async Task<Customer> GetAsync(Expression<Func<Customer, bool>> expression)
{
var result = await DbContext.Customers.Where(expression).FirstOrDefaultAsync();
return result;
}
public async Task<List<Customer>> ListAsync(Expression<Func<Customer, bool>> expression)
{
var result = await DbContext.Customers.Where(expression).ToListAsync();
return result;
}
public async Task<Customer> UpdateAsync(Customer entity)
{
var customer= DbContext.Customers.Update(entity);
return customer.Entity;
}
}
with this Interface.
public interface ICustomerService : IAsyncRepository<Entities.Customer.Customer>
{
}
then I want to write some strategy for example.
InMemoryDatabase class for inject DbContext
You can install Microsoft.EntityFrameworkCore.InMemory and Create a Class like below and only change dbcontext name with your own dbcontext then inject this class to each test class that you want to implement.
public class InMemoryRequestDbFixture : IDisposable
{
protected readonly CustomerDbContext DbContext;
public InMemoryRequestDbFixture()
{
var options = new DbContextOptionsBuilder<CustomerDbContext>()
.UseInMemoryDatabase($"RequestDb-{Guid.NewGuid()}")
.ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
DbContext = new CustomerDbContext(options);
DbContext.Database.EnsureCreated();
SeedDatabase();
}
private void SeedDatabase()
{
var data = new Domain.Entities.Customer.Customer
(
PhoneNumber.Create("+989144967941"),
Email.Create("soshyant@gmail.com"),
BankAccountNumber.Create("3333333333333"),
new Domain.Entities.Customer.CustomerInfo("tohid", "haghighi", DateTime.Now)
);
DbContext.Customers.Add(data);
DbContext.SaveChanges();
}
public void Dispose()
{
DbContext.Database.EnsureDeleted();
DbContext.Dispose();
}
}
inject this to test class.
public class CustomerService_AddCustomer_InsertUniqueEmail_Success : InMemoryRequestDbFixture
{
[Fact]
public async Task CustomerServiceAddCustomerInsertUniqueEmailSuccess()
{
string error_Message = "";
// Arrange
var data = new Domain.Entities.Customer.Customer
(
PhoneNumber.Create("+989144967941"),
Email.Create("soshyant@gmail.com"),
BankAccountNumber.Create("3333333333333"),
new Domain.Entities.Customer.CustomerInfo("tohid", "haghighi", DateTime.Now)
);
var service = new CustomerService(DbContext);
// Act
try
{
var customer = await service.AddAsync(data);
}
catch (Exception ex)
{
error_Message= ex.Message;
}
// Assert
Assert.NotEqual(error_Message,"");
}
}