我正在使用通用存储库模式Repository<TEntity>
,其中存储库通过上下文访问实体。然后我有一个服务层接受构造函数中的上下文。现在,我可以在服务中拥有多个存储库,通过相同的上下文访问实体。很标准。这非常适合映射到实体的表/视图,但我不能对通过存储过程的数据进行单元测试。
这是我目前的设置:
IDbContext:
public interface IDbContext : IDisposable
{
IDbSet<T> Set<T>() where T : class;
DbEntityEntry<T> Entry<T>(T entity) where T : class;
void SetModified(object entity);
int SaveChanges();
// Added to be able to execute stored procedures
System.Data.Entity.Database Database { get; }
}
上下文:
public class AppDataContext : DbContext, IDbContext
{
public AppDataContext()
: base("Name=CONNECTIONSTRING")
{
base.Configuration.ProxyCreationEnabled = false;
}
public new IDbSet<T> Set<T>() where T : class
{
return base.Set<T>();
}
public void SetModified(object entity)
{
Entry(entity).State = EntityState.Modified;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new BookingMap());
}
// Added to be able to execute stored procedures
System.Data.Entity.Database Database { get { return base.Database; } }
}
通用存储库:
public class Repository<T> : IRepository<T> where T : class
{
private readonly IDbContext context;
public Repository(IDbContext context)
{
this.context = context;
}
public IQueryable<T> GetAll()
{
return this.context.Set<T>().AsQueryable();
}
public void Add(T entity)
{
this.context.Set<T>().Add(entity);
}
public void Delete(T entity)
{
this.context.Set<T>().Remove(entity);
}
public void DeleteAll(IEnumerable<T> entities)
{
foreach (var e in entities.ToList())
{
this.context.Set<T>().Remove(e);
}
}
public void Update(T entity)
{
this.context.Set<T>().Attach(entity);
this.context.SetModified(entity);
}
public void SaveChanges()
{
this.context.SaveChanges();
}
public void Dispose()
{
if (this.context != null)
{
this.context.Dispose();
}
}
}
服务:
public class BookingService
{
IDbContext _context;
IRepository<Booking> _bookingRepository;
public BookingService(IDbContext context)
{
_context = context;
_bookingRepository = new Repository<Booking>(context);
}
public IEnumerable<Booking> GetAllBookingsForName(string name)
{
return (from b in _bookingRepository.GetAll()
where b.Name == name
select b);
}
}
测试:
[TestClass]
public class BookingServiceTest
{
[TestMethod]
public void Test_Get_All_Bookings_For_Name()
{
var mock = new Mock<IDbContext>();
mock.Setup(x => x.Set<Booking>())
.Returns(new FakeDbSet<Booking>
{
new Booking { Name = "Foo" },
new Booking { Name = "Bar" }
});
BookingService _bookingService = new BookingService(mock.Object);
var bookings = _bookingService.GetAllBookingsForName(name);
Assert.AreEqual(2, bookings.Count(), "Booking count is not correct");
}
}
这非常适合映射到实体的表/视图,但我无法对通过存储过程发送的数据进行单元测试。
我在互联网上查找了DbContext.Database
属性,我可以使用.SqlQuery()
函数执行存储过程,并将它们映射到实体类型。
这是我添加到Repository<T>
类的内容:
public IEnumerable<T> SqlQuery(string storedProc, params object[] paramList)
{
return this.context.Database.SqlQuery<T>(storedProc, paramList);
}
并在我的服务类中调用.SqlQuery()
函数:
public IEnumerable<Booking> GetAllBookings(string name)
{
return _bookingRepository.SqlQuery("EXEC GetAllBookings @name = {0}", name);
}
这很有效(我能够获得一些数据),但我的问题是如何模拟和单元测试呢?
答案 0 :(得分:8)
我刚刚遇到了这样做的需要,我的谷歌搜索让我想到了这个问题。我不喜欢Sriram Sakthivel的回答,当我已经有一个抽象时,我不想引入另一个抽象:
我已经有了一个我从DbContext
中提取的界面,并在test double中实现。
我只是将int ExecuteSqlCommand(string sql, params object[] parameters)
添加到我的界面,在实际的上下文中我实现了它:
public int ExecuteSqlCommand(string sql, params object[] parameters)
{
return Database.ExecuteSqlCommand(sql, parameters);
}
显然,只需委托实际的EF Database
属性来完成工作。
在我的测试中,我实现了它:
public int ExecuteSqlCommand(string sql, params object[] parameters)
{
return 0;
}
哪个没有真正做任何事情,重点是:你不是单元测试实际的存储过程,你只需要一种方法让它返回一些有用的东西。
我想在某些时候我可能需要它在单元测试中返回0以外的东西,此时我可能会引入类似Func<int> executeSqlCommandResultFactory
的东西来测试双构造函数以便我可以控制它,但目前YAGNI适用。
答案 1 :(得分:5)
您可以使用某个界面Database
和IDatabase
方法抽象出SqlQuery
属性。
interface IDatabase
{
public IEnumerable<T> SqlQuery<T>(string sql, params Object[] parameters);
}
class DatabaseWrapper : IDatabase
{
private readonly Database database;
public DatabaseWrapper(Database database)
{
this.database = database;
}
public IEnumerable<T> SqlQuery<T>(string sql, params Object[] parameters)
{
return database.SqlQuery<T>(storedProc, paramList);
}
}
修改IDbContext
接口以使用IDatabase
而不是具体实例,以便我们可以模拟它。
public interface IDbContext : IDisposable
{
...
// Added to be able to execute stored procedures
IDatabase Database { get; }
}
以这种方式实施
public class AppDataContext : DbContext, IDbContext
{
private readonly IDatabase database;
public AppDataContext()
: base("Name=CONNECTIONSTRING")
{
base.Configuration.ProxyCreationEnabled = false;
this.database = new DatabaseWrapper(base.Database);
}
...
// Added to be able to execute stored procedures
IDatabase Database { get { return database; } }
}
此时我相信您知道如何模拟IDatabase
以返回测试数据。
答案 2 :(得分:0)
我意识到这是一个古老的问题,但是对于有类似问题的任何人,这是我的看法。
为什么不仅仅使用AutoFixture为通常从存储过程返回的数据创建对象并模拟存储库以返回数据?
public class FooBar
{
private Fixture fixture;
private Mock<BookingRepository> bookingRepository;
public FooBar()
{
fixture = new Fixture();
bookingRepository= new Mock<BookingRepository>();
}
public void TestInitialize()
{
var sprocObject = fixture.Create<DbObject>();
bookingRepository.Setup(x => x.GetAllBookings(It.IsAny<string>())).Returns(sprocObject);
}
// Unit tests
}
正如Gert Arnold所说,如果要测试实际的存储过程,则应该进行集成测试。如果要测试服务/存储库逻辑,则只需要返回一些数据即可。