Moqing Entity Framework 6 .Include (), utilizzando DbSet<>

Mi piacerebbe fare da sfondo a questa domanda. Saltare, se volete. Per un po ‘ ho pagato molta attenzione a dibattiti su stackoverflow e altrove, per quanto riguarda i test di codice relativa a EF. Un campo dice, prova direttamente con un database a causa delle differenze tra il Linq to Objects & Sql e implementazioni. Dice un altro test da beffardo.

Un altro split, che secondo me è il problema dell’utilizzo di repository, o accettare che DbContext e DbSet già fornire un’unità di lavoro e modello di repository. Nel tempo che sono stato con EF, ho provato ogni combinazione di opinioni fornite da questi campi. A prescindere di quello che ho fatto, EF dimostra di essere difficile prova.

Ero entusiasta di trovare la squadra EF fatto DbSet più mockable in EF 6. Hanno anche fornito documentazione su come finto DbSet, compresi i metodi asincroni utilizzando Moq. A lavorare sul mio ultimo progetto che coinvolge le Api Web ho capito che se avessi finto EF, potrei saltare la scrittura di repository, come la ragione per la scrittura di loro è di fare le cose verificabili. L’ispirazione è nata dopo aver letto alcuni post del blog come questo…

–Fine di background —

Il vero problema è che seguendo l’esempio di codice dato dal team EF su come Moq DbSet, se .Include() è utilizzato in alcun codice, un ArgumentNullException è gettato.

Altri related post COSÌ

Qui è la mia interfaccia per DbContext:

public interface ITubingForcesDbContext
{
    DbSet<WellEntity> Wells { get; set; }

    int SaveChanges();

    Task<int> SaveChangesAsync();

    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

Questa è l’ente principale che il mio controller si occupa

public class WellEntity
{
    public int Id { get; set; }
    public DateTime DateUpdated { get; set; }
    public String UpdatedBy { get; set; }

    [Required]
    public string Name { get; set; }
    public string Location { get; set; }

    public virtual Company Company { get; set; }

    public virtual ICollection<GeometryItem> GeometryItems
    {
        get { return _geometryItems ?? (_geometryItems = new Collection<GeometryItem>()); }
        protected set { _geometryItems = value; }
    }
    private ICollection<GeometryItem> _geometryItems;

    public virtual ICollection<SurveyPoint> SurveyPoints
    {
        get { return _surveyPoints ?? (_surveyPoints = new Collection<SurveyPoint>()); }
        protected set { _surveyPoints = value; }
    }
    private ICollection<SurveyPoint> _surveyPoints;

    public virtual ICollection<TemperaturePoint> TemperaturePoints
    {
        get { return _temperaturePoints ?? (_temperaturePoints = new Collection<TemperaturePoint>()); }
        protected set { _temperaturePoints = value; }
    }
    private ICollection<TemperaturePoint> _temperaturePoints;
}

Qui è il controller che utilizza direttamente un EF DbContext

 [Route("{id}")]
 public async Task<IHttpActionResult> Get(int id)
 {
        var query = await TheContext.Wells.
                                   Include(x => x.GeometryItems).
                                   Include(x => x.SurveyPoints).
                                   Include(x => x.TemperaturePoints).
                                   SingleOrDefaultAsync(x => x.Id == id);
        if (query == null)
        {
            return NotFound();
        }
        var model = ModelFactory.Create(query);
        return Ok(model);
}

Infine ecco il test fallito…

Configurazione Di Prova—

   [ClassInitialize]
   public static void ClassInitialize(TestContext testContest)
        {

            var well1 = new WellEntity { Name = "Well 1" };
            var well2 = new WellEntity { Name = "Well 2" };
            var well3 = new WellEntity { Name = "Well 3" };
            var well4 = new WellEntity { Name = "Well 4" };

            well1.GeometryItems.Add(new GeometryItem());
            well1.TemperaturePoints.Add(new TemperaturePoint());
            well1.SurveyPoints.Add(new SurveyPoint());

            well2.GeometryItems.Add(new GeometryItem());
            well2.TemperaturePoints.Add(new TemperaturePoint());
            well2.SurveyPoints.Add(new SurveyPoint());

            well3.GeometryItems.Add(new GeometryItem());
            well3.TemperaturePoints.Add(new TemperaturePoint());
            well3.SurveyPoints.Add(new SurveyPoint());

            well4.GeometryItems.Add(new GeometryItem());
            well4.TemperaturePoints.Add(new TemperaturePoint());
            well4.SurveyPoints.Add(new SurveyPoint());

            var wells = new List<WellEntity> { well1, well2, well3, well4 }.AsQueryable();

            var mockWells = CreateMockSet(wells);

            _mockContext = new Mock<ITubingForcesDbContext>();
            _mockContext.Setup(c => c.Wells).Returns(mockWells.Object);
   }

   private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> data) where T : class
    {
        var mockSet = new Mock<DbSet<T>>();

        mockSet.As<IDbAsyncEnumerable<T>>()
            .Setup(m => m.GetAsyncEnumerator())
            .Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));

        mockSet.As<IQueryable<T>>()
               .Setup(m => m.Provider)
               .Returns(new TestDbAsyncQueryProvider<T>(data.Provider));

        mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression);
        mockSet.As<IQueryable<T>>().Setup(m =>m.ElementType).Returns(data.ElementType);
        mockSet.As<IQueryable<T>>().Setup(m=>m.GetEnumerator()).
        Returns(data.GetEnumerator());

        return mockSet;
   }

  [TestMethod]  
   public async Task Get_ById_ReturnsWellWithAllChildData()
    {
        //Arrange
        var controller = new WellsController(_mockContext.Object);

        //Act
        var actionResult = await controller.Get(1);

        //Assert
        var response = actionResult as OkNegotiatedContentResult<WellModel>;
        Assert.IsNotNull(response);
        Assert.IsNotNull(response.Content.GeometryItems);
        Assert.IsNotNull(response.Content.SurveyPoints);
        Assert.IsNotNull(response.Content.TemperaturePoints);
   }

TestDbAsyncQueryProvider & TestDbAsyncEnumerator provengono direttamente dal riferimento EF team di documentazione. Ho provato diverse varianti per come creare i dati per il mock, non ha avuto fortuna con esso.

  • Ho appena provato a configurare async test (Aggiunto al progetto Github : github.com/pauldambra/includeTests) e funzionano bene utilizzando lo stesso beffardo che hai usato. Siete in grado di spingere un esempio di fallimento di Github o aggiungere ulteriori dettagli su dove il test è fallito. Questo si sente come il programma di installazione ha in realtà un valore null in cui non ci dovrebbe essere…
  • Ciao Paolo, grazie per il vostro aiuto con questo… Per rendere il vostro esempio, rappresentano il mio codice, è necessario modificare IDbSet per DbSet, e che vi costringerà a cambiare i CreateMockSet metodo per corrispondere a ciò che ho postato sopra. Poi si vedrà lo stesso comportamento che sto vedendo, che è di Sistema.ArgumentNullException var queryTask = attendono mockContext.Oggetto.Genitori.Includere(p => p.Bambini).FirstAsync();
  • I metodi di DbSet non virtuale, il che significa che quando Moq deride la classe non può ignorare tali metodi. IDbSet esiste (almeno in parte) per consentire di mock più facilmente. Perché vuoi usare DbSet e non IDbSet?
  • Sullo sfondo c’è un link “DbSet più mockable’, in questo articolo si descrive dice questo.. “Anche se si volesse creare il proprio falsi (o doppio test) in EF6, si può fare con DbSet ora, non IDbSet. IDbSet è ancora lì per la compatibilità all’indietro.” Con EF 6 vogliono beffardo DbSet, non IDbSet, e quando ho guardato tutti i DbSet membri sono contrassegnati come virtuale.
  • Qui è qualcun altro con lo stesso problema. social.msdn.microsoft.com/Forums/en-US/…
  • Ah, quindi sono! Essi non sono elencate come virtuale in prima pagina per DbSet e non ho scavare nel metodi di se stessi… il mio cattivo
  • Yep, Includono non è Virtuale…

InformationsquelleAutor GetFuzzy | 2013-12-11

 

5 Replies
  1. 38

    Per chi si imbatte in questo problema con l’interesse su come risolvere il .Include("Foo") problema con NSubstitute e Entity Framework 6+, sono stato in grado di ignorare il mio Include chiamate nel modo seguente:

    var data = new List<Foo>()
    {
        /* Stub data */
    }.AsQueryable();
    
    var mockSet = Substitute.For<DbSet<Foo>, IQueryable<Foo>>();
    ((IQueryable<Post>)mockSet).Provider.Returns(data.Provider);
    ((IQueryable<Post>)mockSet).Expression.Returns(data.Expression);
    ((IQueryable<Post>)mockSet).ElementType.Returns(data.ElementType);
    ((IQueryable<Post>)mockSet).GetEnumerator().Returns(data.GetEnumerator());
    
    //The following line bypasses the Include call.
    mockSet.Include(Arg.Any<string>()).Returns(mockSet);
    • Si può utilizzare sostituire “Bar” con Esso.IsAny<String>() per renderlo più generale. Poi si può chiamare .Includere(x=>x.Bar), perché le chiamate .Include(“Bar”) internamente
    • Questo in realtà ha funzionato per me, ma forse non sto seguendo esattamente. Intendi utilizzare .Include(Args.Any<string>()) in sostituzione per .Include("Bar"), perché questo non ha funzionato per me 🙁
    • Mi dispiace, io sto usando il Moq. Ma sì, avete capito bene. La linea che uso io e che funziona per me è: dbSet.Setup(m => m.Include(It.IsAny<String>())).Returns(dbSet.Object); dove dbSet = new Mock<DbSet<T>>(); Forse il motivo che non ho il cast IQueryable è rilevante.
    • No, questo non funziona. Mentre, si genera un Sistema.NotSupportedException dicendo che Espressione fa riferimento a un metodo che non appartengono alla deriso oggetto. Tutte le idee su come risolvere questo problema e di farlo funzionare ?
    • Ok ho preso in giro l’eccezione. La chiave per evitare che è quello di non gettare la dbset come un iQueryable come le guide ufficiali di stato.
    • Aggiunto @SalmanHasratKhan ‘s soluzione per la risposta.
    • funziona davvero, la includono la chiamata viene bypassato
    • Scusate ragazzi/ragazze, qual è il ‘Post’ in questo contesto? Ho corretto assumendo che dovrebbe essere “Pippo”?
    • Il test ha esito negativo in quanto si genera un errore: Invalid programma di installazione su un metodo di estensione: x => x.Include<Tasso di>(Essa.IsAny<String>())

  2. 31

    Qui è un esempio completo di utilizzo Moq. È possibile incollare l’intero esempio nella tua classe di unit test. Grazie per i commenti di @jbaum012 e @Skuli. Ho anche consigliamo di ottimo tutorial da Microsoft.

    //An Address entity
    public class Address
    {
        public int Id { get; set; }
        public string Line1 { get; set; }
    }
    
    //A Person referencing Address
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual Address Address { get; set; }
    }
    
    //A DbContext with persons and devices
    //Note use of virtual (see the tutorial reference)
    public class PersonContext : DbContext
    {
        public virtual DbSet<Person> Persons { get; set; }
        public virtual DbSet<Address> Addresses { get; set; }
    }
    
    //A simple class to test
    //The dbcontext is injected into the controller
    public class PersonsController
    {
        private readonly PersonContext _personContext;
    
        public PersonsController(PersonContext personContext)
        {
            _personContext = personContext;
        }
    
        public IEnumerable<Person> GetPersons()
        {
            return _personContext.Persons.Include("Address").ToList();
        }
    }
    
    //Test the controller above
    [TestMethod]
    public void GetPersonsTest()
    {
        var address = new Address { Id = 1, Line1 = "123 Main St." };
        var expectedPersons = new List<Person>
        {
            new Person { Id = 1, Address = address, Name = "John" },
            new Person { Id = 2, Address = address, Name = "John Jr." },
        };
    
        var mockPersonSet = GetMockDbSet(expectedPersons.AsQueryable());
        mockPersonSet.Setup(m => m.Include("Address")).Returns(mockPersonSet.Object);
    
        var mockPersonContext = new Mock<PersonContext>();
        mockPersonContext.Setup(o => o.Persons).Returns(mockPersonSet.Object);
    
        //test the controller GetPersons() method, which leverages Include()
        var controller = new PersonsController(mockPersonContext.Object);
        var actualPersons = controller.GetPersons();
        CollectionAssert.AreEqual(expectedPersons, actualPersons.ToList());
    }
    
    //a helper to make dbset queryable
    private Mock<DbSet<T>> GetMockDbSet<T>(IQueryable<T> entities) where T : class
    {
        var mockSet = new Mock<DbSet<T>>();
        mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(entities.Provider);
        mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(entities.Expression);
        mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(entities.ElementType);
        mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(entities.GetEnumerator());
        return mockSet;
    }
    • La linea specifica che ha fatto il trucco per me è stato mockPersonSet.Di installazione(m => m.Include(“Indirizzo”)).Restituisce(mockPersonSet.Oggetto);
  3. 8

    A giocare con questo e di riferimento le risposte qui Installazione del risultato per la chiamata al metodo di estensione sembra Moq non mock statico metodi di estensione

    Ho provato ad aggiungere:

    mockSet.Setup(t => t.FirstAsync()).Returns(Task.FromResult(data.First()));
    mockSet.Setup(t => t.FirstAsync(It.IsAny<Expression<Func<T, bool>>>())).Returns(Task.FromResult(data.First()));

    E Moq si lamenta che:

    Sistema.NotSupportedException : Espressione fa riferimento a un metodo che
    non appartiene a schernito oggetto: t => t.FirstAsync()

    Quindi sembra che ci sono tre opzioni:

    1. il refactoring del codice per isolare ulteriormente il dbcontext quindi non è necessario
      prova questo comportamento
    2. passare da DbSet per IDbSet invece di scherno
      DbContext
    3. consentire il test per creare un database di SQL database compatto e
      popolarlo con i dati per eseguire il test
    • Paolo, grazie per il vostro aiuto, ero arrivato a quella conclusione, la scorsa notte, e sono andato indietro e guardò quello che sto facendo per vedere se è davvero necessario utilizzare .Include() a tutti. Io ho optato per lazy-loading, come sto facendo questo per una web API controller, e quando ho generare i miei modelli di inviare attraverso la rete, la navigazione proprietà si accede. Vale a dire, le proprietà di navigazione innescare la seconda query al db, prima che l’oggetto si dirige sopra il filo. Finora questo sembra accettabile in termini di prestazioni e mi consente di continuare con questo percorso facile per i test… vorrei che EF è stato più facile di testare…
    • Ciao Paolo, una nota di più, qualcuno del team EF tornato a me, non è ovvio, ma a quanto pare queste cose possono essere deriso… date un occhiata alla “Moq_DbSet_can_be_used_for_query_with_include_extension_method_that_does_something” in MockableDbSetTests in EF FunctionalTests progetto. Grazie, Arthur
    • Per chi volesse arrivare in ritardo a questa discussione. La soluzione qui: entityframework.codeplex.com/SourceControl/latest#test/… utilizza il Include overload che accetta un string non lambda sovraccarico.
    • Strettamente parlando, la risposta è sbagliato ora la domanda sui Enity Framework 6, e baum012 la risposta di opere per che (+ vedere Skuli commento per Moq versione)
    • Bene, è di due anni. 🙂
    • Sto cercando di testare un metodo che chiama: .Include(n => n.Something.Select( q => q.SomethingElse)); tutte le idee?

  4. 0

    Sono riuscito a finto Includere nel Moq con un approccio generico. Anche se questo non copre tutti gli usi di Include(), solo con stringhe e di Espressione, ma è più adatto alle mie esigenze:

    public Mock<DbSet<T>> SetupMockSetFor<T>(Expression<Func<DbContext, DbSet<T>>> selector) where T : class
        {
            var mock = new Mock<DbSet<T>>();
    
            mock.ResetCalls();
    
            this.EntitiesMock.Setup(m => m.Set<T>()).Returns(mock.Object);
            this.EntitiesMock.Setup(selector).Returns(mock.Object);
    
            mock.Setup(x => x.Include(It.IsAny<string>())).Returns(mock.Object);
    
            try
            {
                mock.Setup(x => x.Include(It.IsAny<Expression<Func<T, object>>>()))
                    .Returns(mock.Object);
            }
            catch
            {
                //Include only applies to some objects, ignore where it doesn't work
            }
    
            return mock;
        }

    test di utilizzo:

            var mockCourseSet = SetupMockSetFor(entities => entities.Courses);

    Nel metodo di servizio:

    var foundCourses = dbContext.Courses.Include(c => c.CourseParticipants).Where(c => c.Id = courseId)

Lascia un commento