huysentruitw / entity-framework-mock

Easy Mock wrapper for mocking EF6 DbContext and DbSet using Moq or NSubstitute

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Removing Cascade-Delete Conventions

jsoules opened this issue · comments

This tool looks like exactly what I need to test an existing app that's gone without tests for far too long. However, I'm running into an issue with a test in which Entity thinks there's a possible cycle with the default cascade delete convention.

In the DbContext class I'm mocking, I have an OnModelCreating() that removes ManyToManyCascadeDeleteConvention and OneToManyCascadeDeleteConvention, so the code works fine in practice. However, the tests are failing due to the potential cyclical cascade delete.

What's your recommended way to turn off these cascade-delete conventions for the mocked DbContext?

Thanks for your help.

Hi, thanks you for reaching out. Would it be possible to create an isolated unit-test (or small repro) that fails for your scenario?

Thanks for your response! I was working up code to repro and having a lot of issues with the in-memory DB connection thinking the model had changed--odd, since there was no persistent database in the reproducing test error--so I decided to retest in the real application, and now the same code is passing. I didn't change anything, but if I can't reproduce it, there's nothing to share.

Thanks for your help--I'll chalk this one up to gremlins and reach out again if I find something I can actually reproduce reliably.

Hi,
I'm not able to reproduce exactly the issue that I was experiencing, however I'm seeing a range of tests that sometimes work and sometimes don't with no code changes and a variety of errors. I think ultimately the error messages are not accurate, and there's just something not quite working correctly with a local SQLite DB.

This originally appeared as a complaint about foreign-key relationships and the cascade-delete convention potentially causing delete cycles. However, I think it's more general instability because I was not able to reproduce that error message, and I closed this issue when the problem disappeared without me changing anything. This morning, however, various problems are back again.

Here's an example of a test that fails--I included more of the data model than strictly necessary because I was originally trying to repro a FK error. But now the same test is failing because of a login failure to the database--odd, since the DbContext is supposed to be a mock... also the same code was passing when I closed the issue on Tuesday.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Data.Entity.Validation;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using EntityFrameworkMock;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;

namespace ConsoleApp1
{
    public static class MainClass
    {
        public static void Main()
        {
            Database.SetInitializer<MyContext>(null);
            Console.Write("Hello World");
        }
    }

    [Serializable] // custom exception class expected to be thrown
    public class RecordNotFoundException : Exception
    {
        public RecordNotFoundException() { }
        public RecordNotFoundException(string msg) : base(msg) { }
        public RecordNotFoundException(string msg, Exception inner) : base(msg, inner) { }
        protected RecordNotFoundException(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) { }
    }

    // Data Models

    public class Fair
    {
        [Key]
        public int FairId { get; set; }

        [Required]
        public int Year { get; set; }

        [Required]
        public int DefaultInterviewLength { get; set; }

        [Required]
        public int InterviewBreakLength { get; set; }
    }

    public class Employer
    {
        [Key] public int EmployerId { get; set; }
        [Required] public string Name { get; set; }
    }

    public class EmployerFair
    {
        [Key]
        public int EmployerFairId { get; set; }
        [Required]
        public Employer Employer { get; set; }
        [Required]
        public Fair Fair { get; set; }
        [Required]
        public int InterviewLength { get; set; }
        [Required]
        public EmployerRepresentative PrimaryContact { get; set; }
    }

    public class EmployerRepresentative
    {
        [Key]
        public int EmployerRepresentativeId { get; set; }

        [Required, Index(IsUnique = true), StringLength(40)]
        public string Guid { get; set; }

        public DateTime StatusChangeDate { get; set; }
    }


    public class Opportunity
    {
        [Key]
        public int OpportunityId { get; set; }

        [Required]
        public Employer Employer { get; set; }

        [Required]
        public Fair Fair { get; set; }

        [Required]
        public string Title { get; set; }

        [Required]
        public string Description { get; set; }

        public Reservation Reservation { get; set; }
    }

    public class Reservation
    {
        public int ReservationId { get; set; }
        [Required]
        public Employer Employer { get; set; }
        public IList<Opportunity> Opportunities { get; set; }
    }


    // DBContext and its interface
    public interface IMyContext : IDisposable
    {
        DbSet<Fair> Fairs { get; set; }
        DbSet<Opportunity> Opportunities { get; set; }
        int SaveChanges();
        DbSet Set(Type entityType);
        DbEntityEntry Entry(object entity);
        DbSet<T> Set<T>() where T : class;
    }

    public class MyContext : DbContext, IMyContext
    {
        public MyContext() : this("MyContext")
        { }

        public MyContext(string connString) : base(connString)
        {
            Database.SetInitializer<MyContext>(null);
        }
        public virtual DbSet<Fair> Fairs { get; set; }
        public virtual DbSet<Opportunity> Opportunities { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
            modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
        }
    }


    // REPO UNDER TEST
    public class FairRepository
    {
        public FairRepository(IMyContext ctxt)
        {
            Context = ctxt;
        }

        protected IMyContext Context { get; }

        public virtual int Count()
        {
            return Context.Set<Fair>().Count();
        }

        public virtual IQueryable<Fair> FindAll()
        {
            var query = Context.Set<Fair>().AsQueryable();

            return query;
        }

        public virtual Fair FindById(int id)
        {
            var q = Context.Set<Fair>();
            var result = q.Where(IdMatchesModelLambda(id)).ToList();

            var msg = $"Error retrieving {typeof(Fair)} with key {id}\n";
            if (!result.Any())
                throw new RecordNotFoundException(msg);

            return result.First();
        }

        public virtual int Commit()
        {
            try
            {
                return Context.SaveChanges();
            }
            catch (DbEntityValidationException validationEx)
            {
                foreach (var validationErrs in validationEx.EntityValidationErrors)
                {
                    var errMsg = new StringBuilder("Validation failed for entities:\n");
                    foreach (var validationErr in validationErrs.ValidationErrors)
                        errMsg.AppendFormat("\tProperty: {0} Error: {1}\n",
                            validationErr.PropertyName, validationErr.ErrorMessage);
                }

                throw;
            }
        }

        private Expression<Func<Fair, bool>> IdMatchesModelLambda(int id)
        {
        var objectContext = ((IObjectContextAdapter)Context).ObjectContext;
        var set = objectContext.CreateObjectSet<Fair>();
            var primaryKey = set.EntitySet.ElementType.KeyMembers
                                .Select(k => k.Name)
                                .First();
            var modelType = Expression.Parameter(typeof(Fair));
            var modelId = Expression.MakeMemberAccess(modelType, typeof(Fair).GetProperty(primaryKey));
            var idToCompare = Expression.Constant(id);
            var equalityCheck = Expression.Equal(modelId, idToCompare);
            var inputMatchesModelLambda = Expression.Lambda<Func<Fair, bool>>(equalityCheck, modelType);

            return inputMatchesModelLambda;
        }
    }


    // UNIT TEST ITSELF
    [TestClass]
    public class RepositoryTest
    {

        private FairRepository _repo;

        [TestInitialize()]
        public void SetUp()
        {
            var seedVals = new[]
            {
                new Fair
                {
                    FairId = 1,
                    Year = 2017,
                    DefaultInterviewLength = 20,
                    InterviewBreakLength = 5
                },
                new Fair
                {
                    FairId = 2,
                    Year = 2018,
                    DefaultInterviewLength = 21,
                    InterviewBreakLength = 4
                }
            };

            var mock = new DbContextMock<MyContext>("fake connection2");
            var _ = mock.CreateDbSetMock(x => x.Fairs, seedVals);

            _repo = new FairRepository(mock.Object);
        }


        [TestMethod]
        public void TestCount() // WORKS
        {
            var count = _repo.Count();
            Assert.IsTrue(count == 2);
        }

        [TestMethod, ExpectedException(typeof(RecordNotFoundException))]
        public void TestRecordNotFoundById() // FAILS, with various errors
        {
            _repo.FindById(id: 500);
        }
    }
}

I got same error and I resolved with moq.protected.

            mockContext.Protected().Setup("OnModelCreating", ItExpr.IsAny<DbModelBuilder>())
                       .Callback<DbModelBuilder>((DbModelBuilder model) => 
                                                     model.Conventions.Remove<OneToManyCascadeDeleteConvention>());