Entity Framework mocking requires global context - c#

I have recently began to dig into Entity Framework unit-testing with Entity Framework 6 mocking.
I have noticed the following thing:
Entity Framework mocking forces me to create a global context in my BL class, for example:
public class RefundRepayment : IDisposable
{
protected DbContext _dbContext = new DbContext();
/* more properties and class code */
public void Dispose()
{
_dbContext.Dispose();
}
}
I can't quite figure it out, as I'd rather implement the using statement in every method in order to deal with the DbContext, my code will look like:
public class RefundRepayment
{
/* more properties and class code */
public void AccessDb()
{
using(DbContext dbContext = new DbContext())
{
/* db code here */
}
}
}
Is there any specific reason why should we initialize a global context instead of implementing the using statement?

First off, you need to be using DI (via ninject, Unity, Core, etc) to pull this off.
Let me show you a simple sample of an EF GetAll() testing my MVC controller.
[Fact]
public void GetAllOk()
{
// Arrange
// Act
var result = _controller.GetAll() as OkObjectResult;
// Assert
Assert.NotNull(result);
var recordList = result.Value as List<DTO.Account>;
Assert.NotNull(recordList);
Assert.Equal(4, recordList.Count);
}
It relies on this startup code...
public class AccountsControllerTests
{
DatabaseFixture _fixture;
AccountsControllerV1 _controller;
public AccountsControllerTests(DatabaseFixture fixture)
{
_fixture = fixture;
_controller = new AccountsControllerV1(_fixture._uow);
}
What is DatabaseFixture? Glad you asked...
public class DatabaseFixture : IDisposable
{
public ApplicationDbContext _context;
public DbContextOptions<ApplicationDbContext> _options;
public IUoW _uow;
public DatabaseFixture()
{
var x = Directory.GetCurrentDirectory();
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.Tests.json", optional : true)
.Build();
_options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "ProviderTests")
.Options;
_context = new ApplicationDbContext(_options);
_context.Database.EnsureCreated();
Initialize();
_uow = new UoW(_context);
}
private void Initialize()
{
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 1", AccountID = "", AccountUniqueID = "" });
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 2", AccountID = "", AccountUniqueID = "" });
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 3", AccountID = "", AccountUniqueID = "" });
_context.Accounts.Add(new Entities.Account() { AccountNumber = "Number 4", AccountID = "", AccountUniqueID = "" });
_context.SaveChanges();
}
public void Dispose()
{
// Clean Up
_context.Database.EnsureDeleted();
}
}
[CollectionDefinition("Database Collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
}
A few definitions used in the above code. I used a Unit of Work Pattern that contains references to all my EF repositories. I kept Entity (Database) classes and DTO (Data Transfer Object) Classes separate. I used an in-memory replacement for the EF database that I initialize at the beginning of each run and/or test so that my data is always known. I inject the Database Fixture into my test class (not each test) so I am not creating/destroying constantly. Then I create my controller passing in my database UoW definition.
You're real controller requires injection of the UoW container you've created with the real database. You are merely substituting a controlled database environment for your test.
public AccountsControllerV1(IUoW uow)
{
_uow = uow;
}
And yes, I use versioning for the sharp-eyed. And yes, this is a Core 2 example. Still applicable for EF 6, just need 3rd party DI ;)
And the controller method I am testing?
[HttpGet("accounts", Name ="GetAccounts")]
public IActionResult GetAll()
{
try
{
var recordList = _uow.Accounts.GetAll();
List<DTO.Account> results = new List<DTO.Account>();
if (recordList != null)
{
results = recordList.Select(r => Map(r)).ToList();
}
log.Info($"Providers: GetAccounts: Success: {results.Count} records returned");
return Ok(results);
}
catch (Exception ex)
{
log.Error($"Providers: GetAccounts: Failed: {ex.Message}");
return BadRequest($"Providers: GetAccounts: Failed: {ex.Message}");
}
}

Related

Moq mocking EF DbContext

I have a Repository pattern that interacts with Entity Framework.
I'd like to run some unit tests on the repository, and for this reason, I would like to mock DbContext.
So I've created a unit test project (.Net Core 3.1), using Moq as package for unit testing, everything seems to be ok, but when I perform a .ToListAsync() on my repository it throws the following exception:
System.NotImplementedException : The method or operation is not
implemented. Stack Trace:‚ÄČ
IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken)
ConfiguredCancelableAsyncEnumerable1.GetAsyncEnumerator() EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1
source, CancellationToken cancellationToken)
The source code:
public class Customer
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class CustomersDbContext : DbContext
{
public virtual DbSet<Customer> Customers { get; set; }
public CustomersDbContext(DbContextOptions<Customer> options) : base(options) { }
}
public interface ICustomerRepository
{
Task<IEnumerable<Customer>> GetCustomersAsync(Guid? customerId);
}
public class CustomerRepository : ICustomerRepository
{
private readonly CustomersDbContext _dbContext;
public CustomerRepository(CustomersDbContext dbContext)
{
_dbContext = dbContext;
_dbContext.Database.EnsureCreated();
}
public async Task<IEnumerable<Customer>> GetCustomersAsync(Guid? customerId)
{
IEnumerable<Customer> customers = null;
if (customerId.HasValue)
{
var customer = await _dbContext.Customers.FindAsync(new object[] { customerId.Value }, CancellationToken.None);
if (customer != null)
customers = new List<Customer>() { customer };
}
else
{
customers = await _dbContext.Customers.ToListAsync(CancellationToken.None);
}
return customers;
}
}
public class CustomerServiceUnitTests
{
private Mock<CustomersDbContext> GetCustomerDbContextMock()
{
var data = new List<Customer>()
{
new Customer()
{
Id = Guid.NewGuid(),
Name = "Name 1"
},
new Customer()
{
Id = Guid.NewGuid(),
Name = "Name 2"
}
}.AsQueryable();
var mockSet = new Mock<DbSet<Customer>>();
mockSet.As<IQueryable<Customer>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Customer>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Customer>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Customer>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
var optionsBuilder = new DbContextOptions<CustomersDbContext>();
var mockContext = new Mock<CustomersDbContext>(optionsBuilder);
Mock<DatabaseFacade> databaseFacade = new Mock<DatabaseFacade>(mockContext.Object);
databaseFacade.Setup(d => d.EnsureCreatedAsync(CancellationToken.None)).Returns(Task.FromResult(true));
mockContext.Setup(c => c.Database).Returns(databaseFacade.Object);
mockContext.Setup(c => c.Customers).Returns(mockSet.Object);
return mockContext;
}
[Fact]
public async Task Infrastructure_CustomerRepository_GetAll()
{
var mockContext = this.GetCustomerDbContextMock();
ICustomerRepository customerRepository = new CustomerRepository(mockContext.Object);
var customers = await customerRepository.GetCustomersAsync(null);
Assert.NotNull(customers);
Assert.Equal(2, customers.Count());
}
}
If I send an ID filled to the repository it works fine, so this seems to be not ok only for .ToListAsync().
I'm kinda stuck here, what can I do to overcome this?
You cannot mock DbSet query functionality. This is explained in the docs:
Properly mocking DbSet query functionality is not possible, since queries are expressed via LINQ operators, which are static
extension method calls over IQueryable. As a result, when some
people talk about "mocking DbSet", what they really mean is that they
create a DbSet backed by an in-memory collection, and then evaluate
query operators against that collection in memory, just like a simple
IEnumerable. Rather than a mock, this is actually a sort of fake,
where the in-memory collection replaces the the real database.
In order to execute Asynchronous read operation (ToListAsync()) you need to mock an additional interface called "IDBAsyncQueryProvider".
Here's is the required link you can follow. It is under the heading "Testing with async queries"

EF Core In-Memory Database Not Saving To DbSet

Overview
I am currently unit/integration testing my repository pattern with the in-memory database EF Core provides. I am on version 6.0.1 for the Microsoft.EntityFrameworkCore.InMemory nuget and using Visual Studio 2022. I am running into issues saving data to the database. Below I have included snippets of my code.
My data model:
public class Example : IExample
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
My base repository class:
public class Repository<T> : IRepository<T> where T : class
private readonly DbSet<T> _dbSet;
private readonly DbContext _db;
public Repository(DbContext db)
{
_dbSet = db.Set<T>();
_db = db;
}
public virtual async Task Add(T entity)
{
await _dbSet.AddAsync(entity);
}
public virtual async Task<int> SaveAsync(CancellationToken token = default)
{
return await _db.SaveChangesAsync(token);
}
My repository class:
public class ExampleRepo : Repository<Example>
public ExampleRepo(ExampleContext db) : base(db)
{
}
My DbContext Class
I can show my IEntityConfiguration class for the ExampleBuilder shown below if it is needed but I don't believe that to be the problem.
public class ExampleContext : DbContext
private readonly IHttpContextAccessor? _httpContextAccessor;
public ExampleContext(DbContextOptions<ExampleContext> options) : base(options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
}
public ExampleContext(DbContextOptions<ExampleContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
public DbSet<Example>? Examples { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
var assembly = Assembly.GetAssembly(typeof(ExampleBuilder));
if (assembly is null)
{
throw new DataException("Could not find the assembly containing the designated model builder");
}
builder.ApplyConfigurationsFromAssembly(assembly);
foreach (var entity in builder.Model.GetEntityTypes())
{
entity.SetSchema("dbo");
}
}
public override Task<int> SaveChangesAsync(CancellationToken token = default)
{
foreach (var entry in ChangeTracker.Entries()
.Where(x => x.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
{
var user = _httpContextAccessor?.HttpContext?.User?.Identity?.Name ?? "User";
entry.Property("ModifiedBy").CurrentValue = user;
if (entry.State == EntityState.Added)
{
entry.Property("CreatedBy").CurrentValue = user;
}
if (entry.State != EntityState.Deleted) continue;
entry.State = EntityState.Modified;
entry.Property("IsActive").CurrentValue = false;
}
return base.SaveChangesAsync(token);
}
My DbContext factory method:
private ExampleContext GenerateDbContext()
{
var options = new DbContextOptionsBuilder<ExampleContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ExampleContext(options);
}
My unit/integration test utilizing xUnit and NET6.0
[Fact]
public async Task GetAllEntities_ShouldReturnEntities_WhenEntitiesExist()
{
// Arrange
// I used Bogus nuget for single source for generating valid models
var entity = ExampleFaker.GetModelFaker()
.Generate();
await using var context = GenerateDbContext();
var repo = new ExampleRepo(context);
await repo.Add(entity);
var changes = await repo.SaveAsync();
// Act
// Consulted this link already. Bottom answer is most related
//https://stackoverflow.com/questions/46184937/dbcontext-not-returning-local-objects
var response = await repo.GetAll();
// Assess
TestOutputHelper.WriteLine($"Added entity to repository: {entity.ToJson()}");
TestOutputHelper.WriteLine("Expected entities saved: 1");
TestOutputHelper.WriteLine($"Actual entities saved: {changes}");
TestOutputHelper.WriteLine($"Response: {response?.ToJson()}");
// Assert
Assert.Equal(1, changes);
Assert.NotNull(response);
Assert.NotEmpty(response);
Assert.IsType<List<Example>>(response.ToList());
}
Analysis & Issue
The changes variable returns 1 so I interpret this as EF does not have any issue with my model as well as I would think it successfully saved my model in the in-memory database. However, during my GetAll retrieval, no data is returned. When I debug and look into the repository private members, it shows the DbSet is empty so it is not the GetAll method causing the issue either. Since this is also just within the scope of the unit test, I don't think my Program.cs configuration has anything to do with the issue I am seeing. I have been looking at this for quite a while and can't figure out the small detail I am probably missing for the life of me.
Thank you for your help in advance.

EF Core in-memory database generate System.InvalidOperationException when testing an update operation

I got the following error when I try to test an update operation using Entity Framework core:
System.InvalidOperationException : The instance of entity type 'Companies' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
After doing some research, I tried everything that I found:
Create in scope DB context
deattach and attached the object I want to update from the DB context
Return the object to be updated using "AsNoTracking()" , my repository actually do this.
For the testing I am using EF in-memmory database with it fixture, I am using XUnit and .NET 5.
Can I get any help with this please?
Here is my code:
// The repository I am trying to test
public class RepositoryBase<T> : ICrudRepository<T> where T : class, IModel
{
protected PrjDbContext DatabaseContext { get; set; }
public RepositoryBase(PrjDbContext databaseContext) => DatabaseContext = databaseContext;
protected IQueryable<T> FindAll() => DatabaseContext.Set<T>().AsNoTracking();
protected IQueryable<T> FindBy(Expression<Func<T, bool>> expression) => DatabaseContext.Set<T>().Where(expression).AsNoTracking();
public void Create(T entity) => DatabaseContext.Set<T>().Add(entity);
public void Update(T entity) => DatabaseContext.Set<T>().Update(entity);
public void Delete(T entity) => DatabaseContext.Set<T>().Remove(entity);
public async Task<IEnumerable<T>> ReadAllAsync() => await FindAll().ToListAsync().ConfigureAwait(false);
public async Task<T> ReadByIdAsync(int id) => await FindBy(entity => entity.Id.Equals(id)).FirstOrDefaultAsync().ConfigureAwait(false);
}
//The Database context
public partial class PrjDbContext : DbContext
{
public PrjDbContext()
{
}
public PrjDbContext(DbContextOptions<PrjDbContext> options)
: base(options)
{
}
public virtual DbSet<Companies> Companies { get; set; }
}
// This is my fixture with the in-memory Database
public sealed class PrjSeedDataFixture : IDisposable
{
public PrjDbContext DbContext { get; }
public PrjSeedDataFixture(string name)
{
string databaseName = "PrjDatabase_" + name + "_" + DateTime.Now.ToFileTimeUtc();
DbContextOptions<PrjDbContext> options = new DbContextOptionsBuilder<PrjDbContext>()
.UseInMemoryDatabase(databaseName)
.EnableSensitiveDataLogging()
.Options;
DbContext = new PrjDbContext(options);
// Load Companies
DbContext.Companies.Add(new Companies { Id = 1, Name = "Customer 1", Status = 0, Created = DateTime.Now, LogoName = "FakeLogo.jpg", LogoPath = "/LogoPath/SecondFolder/", ModifiedBy = "Admin" });
DbContext.Companies.AsNoTracking();
DbContext.SaveChanges();
}
public void Dispose()
{
DbContext.Dispose();
}
}
The test method "Update_WhenCalled_UpdateACompanyObject", is not working for me.
// And finally, this is my test class, Create_WhenCalled_CreatesNewCompanyObject pass the test, but Update_WhenCalled_UpdateACompanyObject isn't passing the test.
public class RepositoryBaseCompanyTests
{
private Companies _newCompany;
private PrjDbContext _databaseContext;
private RepositoryBase<Companies> _sut;
public RepositoryBaseCompanyTests()
{
_newCompany = new Companies {Id = 2};
_databaseContext = new PrjSeedDataFixture("RepositoryBase").DbContext;
_sut = new RepositoryBase<Companies>(_databaseContext);
}
[Fact]
public void Create_WhenCalled_CreatesNewCompanyObject()
{
//Act
_sut.Create(_newCompany);
_databaseContext.SaveChanges();
//Assert
Assert.Equal(2, _databaseContext.Companies.Where( x => x.Id == 2).FirstOrDefault().Id);
}
[Fact]
public async void Update_WhenCalled_UpdateACompanyObject()
{
//Arrange
var company = await _sut.ReadByIdAsync(1);
company.Name = "Customer 2";
//_databaseContext.Entry(company).State = EntityState.Detached;
//_databaseContext.Attach(company);
//_databaseContext.Entry(company).State = EntityState.Modified;
//Act
_sut.Update(company);
await _databaseContext.SaveChangesAsync();
//Assert
Assert.Equal("Customer 2", _databaseContext.Companies.Where(x => x.Id == 1).FirstOrDefault().Name);
}
}
If you are using EF Core 5.0 then call DbContext.ChangeTracker.Clear() (or go through DbContext.Entries collection and set state to Detached for earlier ones) after DbContext.SaveChanges(); in PrjSeedDataFixture ctor. Adding/Updating an entry makes it tracked and you are reusing the context that created an entry with Id = 1, so when _sut.Update(company); is called it will try to track it again (since ReadByIdAsync should return an untracked one).
P.S.
Adding an extra repository abstraction layer around EF can be considered as antipattern (because EF already implements repository/UoW patterns) and the issue you are having can be one of the examples of why that is true and why this abstraction can be a leaky one. So if you still decide that having one is a good idea - you need to proceed with caution.

How to dynamically choose a DbContext for API endpoint method

I developed and API that uses a helper class to get the database context for each endpoint function. Now I'm trying to write unit tests for each endpoint and I want to use an In-memory db in my unit test project.
The issue I'm running into is that in order to call the API functions I had to add a constructor to my API controller class. This would allow me to pass the dbContext of the in-memory db to the controller function for it to use. However, since the adding of the constuctor I got the following error when attempting to hit the endpoint:
"exceptionMessage": "Unable to resolve service for type 'AppointmentAPI.Appt_Models.ApptSystemContext' while attempting to activate 'AppointmentAPI.Controllers.apptController'."
UPDATE
controller.cs
public class apptController : Controller
{
private readonly ApptSystemContext _context;
public apptController(ApptSystemContext dbContext)
{
_context = dbContext;
}
#region assingAppt
/*
* assignAppt()
*
* Assigns newly created appointment to slot
* based on slotId
*
*/
[Authorize]
[HttpPost]
[Route("/appt/assignAppt")]
public string assignAppt([FromBody] dynamic apptData)
{
int id = apptData.SlotId;
string json = apptData.ApptJson;
DateTime timeStamp = DateTime.Now;
using (_context)
{
var slot = _context.AppointmentSlots.Single(s => s.SlotId == id);
// make sure there isn't already an appointment booked in appt slot
if (slot.Timestamp == null)
{
slot.ApptJson = json;
slot.Timestamp = timeStamp;
_context.SaveChanges();
return "Task Executed\n";
}
else
{
return "There is already an appointment booked for this slot.\n" +
"If this slot needs changing try updating it instead of assigning it.";
}
}
}
}
UnitTest.cs
using System;
using Xunit;
using AppointmentAPI.Controllers;
using AppointmentAPI.Appt_Models;
using Microsoft.EntityFrameworkCore;
namespace XUnitTest
{
public abstract class UnitTest1
{
protected UnitTest1(DbContextOptions<ApptSystemContext> contextOptions)
{
ContextOptions = contextOptions;
SeedInMemoryDB();
}
protected DbContextOptions<ApptSystemContext> ContextOptions { get; }
private void SeedInMemoryDB()
{
using(var context = new ApptSystemContext(ContextOptions))
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var seventh = new AppointmentSlots
{
SlotId = 7,
Date = Convert.ToDateTime("2020-05-19 00:00:00.000"),
Time = TimeSpan.Parse("08:45:00.0000000"),
ApptJson = null,
Timestamp = null
};
context.AppointmentSlots.Add(seventh);
context.SaveChanges();
}
}
[Fact]
public void Test1()
{
DbContextOptions<ApptSystemContext> options;
var builder = new DbContextOptionsBuilder<ApptSystemContext>();
builder.UseInMemoryDatabase();
options = builder.Options;
var context = new ApptSystemContext(options);
var controller = new apptController(context);
// Arrange
var request = new AppointmentAPI.Appt_Models.AppointmentSlots
{
SlotId = 7,
ApptJson = "{'fname':'Emily','lname':'Carlton','age':62,'caseWorker':'Brenda', 'appStatus':'unfinished'}",
Timestamp = Convert.ToDateTime("2020-06-25 09:34:00.000")
};
string expectedResult = "Task Executed\n";
// Act
var response = controller.assignAppt(request);
Assert.Equal(response, expectedResult);
}
}
}
InMemoryClass.cs
using System;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using AppointmentAPI.Appt_Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace XUnitTest
{
public class InMemoryClass1 : UnitTest1, IDisposable
{
private readonly DbConnection _connection;
public InMemoryClass1()
:base(
new DbContextOptionsBuilder<ApptSystemContext>()
.UseSqlite(CreateInMemoryDB())
.Options
)
{
_connection = RelationalOptionsExtension.Extract(ContextOptions).Connection;
}
private static DbConnection CreateInMemoryDB()
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
}
public void Dispose() => _connection.Dispose();
}
}
The exception suggests that you haven't registered your DBContext in your Startup.cs (as mentioned above). I'd also suggest that you change the name of your private readonly property to something other than DbContext (which is the class name and can get confusing)
Use something like this:
private readonly ApptSystemContext _context;
Besides that, your approach should be changed.
First, you will set the connection string when you register the DBContext. Just let dependency injection take care of that for you. Your controller should look like this:
public apptController(ApptSystemContext dbContext)
{
_context = dbContext;
}
The dbContext won't be null if you register it in Startup.
Next, unit testing is a tricky concept, but once you write your Unit test, you'll start to understand a little better.
You've said that you want to use the SQL In Memory db for unit testing, which is a good approach (be aware that there are limitations to SQL In Mem like no FK constraints). Next, I assume you want to test your Controller, so, since you MUST pass in a DBContext in order to instantiate your Controller, you can create a new DBContext instance that is configured to use the In Memory Database.
For example
public void ApptControllerTest()
{
//create new dbcontext
DbContextOptions<ApptSystemContext> options;
var builder = new DbContextOptionsBuilder<ApptSystemContext>();
builder.UseInMemoryDatabase();
options = builder.Options;
var context = new ApptSystemContext(options);
//instantiate your controller
var controller = new appController(context);
//call your method that you want to test
var retVal = controller.assignAppt(args go here);
}
Change the body of the method to this:
public string assignAppt([FromBody] dynamic apptData)
{
int id = apptData.SlotId;
string json = apptData.ApptJson;
DateTime timeStamp = DateTime.Now;
using (_context)
{
var slot = _context.AppointmentSlots.Single(s => s.SlotId == id);
// make sure there isn't already an appointment booked in appt slot
if (slot.Timestamp == null)
{
slot.ApptJson = json;
slot.Timestamp = timeStamp;
_context.SaveChanges();
return "Task Executed\n";
}
else
{
return "There is already an appointment booked for this slot.\n" +
"If this slot needs changing try updating it instead of assigning it.";
}
}
}
Another suggestion, don't use a dynamic object as the body of a request unless you are absolutely forced to do so. Using a dynamic object allows for anything to be passed in and you lose the ability to determine if a request is acceptible or not.

Mocking EF6 with validation that occurs on SaveChanges

I found a decent article to get me started on unit testing my Entity Framework-based application using Moq: https://msdn.microsoft.com/en-us/data/dn314429.aspx
This issue I'm having is that the SaveChanges method of the Mock does not appear to trigger the ValidateEntity method like it normally would. None of the validation settings I configured in the EntityTypeConfiguration are being thrown as a DbEntityValidationException.
For example, my AddRoles_Fails_For_Empty_Name tests to make sure that the service cannot add a role with an empty name. Either the IsRequired() configuration is not being applied, or the ValidateEntity method is not being called. I should mention that it works correctly if I use the actual context in the web app.
I've included some of my relevant unit testing, DbContext, and Service code below.
Am I doing something incorrectly? Are there any known issues or workarounds?
Role DB Map
public class RoleMap : EntityTypeConfiguration<Role>
{
public RoleMap()
{
ToTable("bm_Roles");
HasKey(r => r.Id);
Property(r => r.Name).IsRequired().HasMaxLength(100).HasIndex(new IndexAttribute("UX_Role_Name") { IsUnique = true });
Property(r => r.Description).HasMaxLength(500);
}
}
DbContext
public class BlueMoonContext : DbContext, IBlueMoonContext
{
public BlueMoonContext() : base("name=BlueMoon")
{
}
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.AddFromAssembly(typeof(BlueMoonContext).Assembly);
}
public void MarkAsModified<T>(T entity) where T : class
{
entity.ThrowIfNull("entity");
Entry<T>(entity).State = EntityState.Modified;
}
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
var result = base.ValidateEntity(entityEntry, items);
if (entityEntry.State == EntityState.Added || entityEntry.State == EntityState.Modified)
{
// Perform validations that require database lookups
if (entityEntry.Entity is Role)
{
ValidateRole((Role)entityEntry.Entity, result);
}
else if (entityEntry.Entity is User)
{
ValidateUser((User)entityEntry.Entity, result);
}
}
return result;
}
private void ValidateRole(Role role, DbEntityValidationResult result)
{
if (role.Name.HasValue() && !Roles.NameAvailable(role.Name, role.Id))
{
result.ValidationErrors.Add(new DbValidationError("Name", "Already in use"));
}
}
private void ValidateUser(User user, DbEntityValidationResult result)
{
if (user.UserName.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("UserName", "Already in use"));
}
if (user.Email.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("Email", "Already in use"));
}
}
}
Account Service
public class AccountService : BaseService, IAccountService
{
private IPasswordHasher _passwordHasher;
public AccountService(IBlueMoonContext context, IPasswordHasher passwordHasher) : base(context)
{
_passwordHasher = passwordHasher;
}
public ServiceResult CreateRole(Role role)
{
role.ThrowIfNull("role");
Context.Roles.Add(role);
return Save();
}
// Copied from base service class
protected ServiceResult Save()
{
var result = new ServiceResult();
try
{
Context.SaveChanges();
}
catch (DbEntityValidationException validationException)
{
foreach (var validationError in validationException.EntityValidationErrors)
{
foreach (var error in validationError.ValidationErrors)
{
result.AddError(error.ErrorMessage, error.PropertyName);
}
}
}
return result;
}
}
Unit Test
[TestFixture]
public class AccountServiceTests : BaseTest
{
protected Mock<MockBlueMoonContext> _context;
private IAccountService _accountService;
[TestFixtureSetUp]
public void Setup()
{
_context = new Mock<BlueMoonContext>();
var data = new List<Role>
{
new Role { Id = 1, Name = "Super Admin" },
new Role { Id = 2, Name = "Catalog Admin" },
new Role { Id = 3, Name = "Order Admin" }
}.AsQueryable();
var roleSet = CreateMockSet<Role>(data);
roleSet.Setup(m => m.Find(It.IsAny<object[]>())).Returns<object[]>(ids => data.FirstOrDefault(d => d.Id == (int)ids[0]));
_context.Setup(m => m.Roles).Returns(roleSet.Object);
// _context.Setup(m => m.SaveChanges()).Returns(0);
_accountService = new AccountService(_context.Object, new CryptoPasswordHasher());
}
[Test]
public void AddRole_Fails_For_Empty_Name()
{
var role = new Role { Id = 4, Name = "" };
var result = _accountService.CreateRole(role);
Assert.False(result.Success);
}
}
SaveChanges is a virtual method which means you invoke a fake method....
You can create your mock CallBase = true, but it is not a good idea(it miss the idea of UT):
_context = new Mock<BlueMoonContext>(){ CallBase = true };
The above code will use the real implementation of BlueMoonContext for any method/property which is not explicitly setup.
RoleMap is responsible for your DB stracture, you should test it as a part of integration test(with DB).
In my opinion you should create an integration tests to verify the integrity(for example; cover RoleMap) with your DB, And create a UT using the Throw setup to cover the catch section(it's a part of your unit):
_contest.Setup(x => x.SaveChanges())
.Throws(new DbEntityValidationException());
Edit to answer the OP question in the comment
no, you don't have to separate the built in validation, you have to create another test(integration test). In this test you'll verify the validation behaviour: insert an illegal entity, expect that exception will raise(using ExpectedExceptionAttribute) and then verify that the DB is empty... to apply this behaviour use this pattern:
try
{
\\...
\\try to commit
}
catch(DbEntityValidationException ex)
{
\\do some validation, then:
throw;\\for ExpectedExceptionAttribute
}
I looked over the api of EntityTypeConfiguration, I didn't saw any contact which allows to UT the rules(unless you use tools like MsFakes, TypeMock Isolator there is no way to verify the ToTable/HasKey/Property was called). The class is being in used inside EntityFramework(which is a part of the BCL) in the integration test you don't have to verify that EntityFramework work properly, you are going to verify that your custom rules was integrated and works as you expect(In this answer you can read the reason not to test a BCL classes).
So use Moq in the UTs of AccountService. Create an integration tests for BlueMoonContext and RoleMap(without Moq).
By the way #LadislavMrnka offer an interesting way to test(integration test) EntityTypeConfiguration

Categories