J'aimerais donner le contexte de cette question. Ignorez si vous le souhaitez. Pendant un certain temps, j'ai prêté une attention particulière aux débats en cours sur stackoverflow et ailleurs concernant les tests de code en ce qui concerne EF. Un camp dit, testez directement une base de données en raison des différences entre Linq to Objects & Sql et les implémentations. Un autre dit tester en se moquant.
Une autre divergence d'opinion est la question de l'utilisation des référentiels ou de l'acceptation que DbContext et DbSet fournissent déjà une unité de travail et un modèle de référentiel. Depuis que j'utilise EF, j'ai essayé toutes les combinaisons d'opinions fournies par ces camps. Indépendamment de ce que j'ai fait, EF s'avère difficile à tester.
J'étais ravi de trouver l'équipe EF faite DbSet plus mockable dans EF 6. Ils ont également fourni documentation sur la façon de se moquer de DbSet, y compris les méthodes asynchrones utilisant Moq. En travaillant sur mon dernier projet impliquant Web Api, j'ai réalisé que si je pouvais me moquer d'EF, je pouvais ignorer l'écriture de référentiels, car la raison normale de les écrire est de rendre les choses testables. L'inspiration est venue après avoir lu quelques articles de blog tels que this ...
- Fin de l'arrière-plan ---
Le problème réel est qu'en suivant l'exemple de code donné par l'équipe EF sur la façon de Moq DbSet, si .Include () est utilisé dans n'importe quel code, une ArgumentNullException est levée.
Autre poste connexe sur SO
Voici mon interface pour DbContext:
public interface ITubingForcesDbContext
{
DbSet<WellEntity> Wells { get; set; }
int SaveChanges();
Task<int> SaveChangesAsync();
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
Ceci est la principale entité avec laquelle mon contrôleur traite
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;
}
Voici le contrôleur qui utilise directement 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);
}
Enfin voici le test qui échoue ...
Configuration de test ---
[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 proviennent directement de la documentation de l'équipe EF référencée. J'ai essayé plusieurs variantes de création de données pour la maquette, mais je n'ai pas eu de chance avec.
Pour tous ceux qui rencontrent ce problème avec intérêt sur la façon de résoudre le problème de .Include("Foo")
avec NSubstitute et Entity Framework 6+, j'ai pu contourner mes appels Include
de la manière suivante:
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);
Voici un exemple complet utilisant Moq. Vous pouvez coller l'exemple complet dans votre classe de test unitaire. Merci aux commentaires de @ jbaum012 et @Skuli. Je recommande également l'excellent tutoriel de 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;
}
Jouer avec cela et référencer les réponses ici résultat de la configuration pour l'appel à la méthode d'extension il semble que Moq ne puisse pas se moquer des méthodes d'extension statiques
J'ai essayé d'ajouter:
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()));
Et Moq se plaint que:
System.NotSupportedException: l'expression fait référence à une méthode qui n'appartient pas à l'objet simulé: t => t.FirstAsync ()
Il semble donc qu'il y ait trois options:
J'ai réussi à me moquer d'Inclure dans Moq avec une approche générique. Bien que cela ne couvre pas toutes les utilisations de Include (), uniquement avec la chaîne et l'expression, mais il convenait à mes besoins:
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;
}
utilisation de test:
var mockCourseSet = SetupMockSetFor(entities => entities.Courses);
Méthode en service:
var foundCourses = dbContext.Courses.Include(c => c.CourseParticipants).Where(c => c.Id = courseId)
L'exemple DbSet
fourni par l'équipe EF n'est que cela: un exemple.
Si vous voulez vous moquer de Include
(ou FindAsync
) , vous devrez le faire vous-même.