在没有依赖注入的情况下使用 Entity Framework 测试服务
Test Service using Entity Framework without dependency injection
我正在尝试测试服务查询中的业务逻辑。所以我不希望我的测试能够真正访问数据库,因为它们是单元测试,而不是集成测试。
所以我做了一个简单的例子来说明我的上下文以及我是如何尝试填充它的。
我有一个实体
public class SomeEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
和一项服务
public class Service
{
public int CountSomeEntites()
{
using (var ctx = new Realcontext())
{
int result = ctx.SomeEntities.Count();
return result;
}
}
}
这才是真正的背景
public partial class Realcontext : DbContext
{
public virtual DbSet<SomeEntity> SomeEntities { get; set; }
public Realcontext() : base("name=Realcontext")
{
InitializeContext();
}
partial void InitializeContext();
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
}
所以我尝试创建一个假上下文,并在我的测试方法中绕过了真实上下文的构造函数
这是假的上下文
public class FakeContext : DbContext
{
public DbSet<SomeEntity> SomeEntities { get; set; }
public FakeContext()
{
}
}
最后是测试class
[TestClass]
public class ServiceTests
{
[TestMethod]
public void CountEmployee_ShoulReturnCorrectResult()
{
using (ShimsContext.Create())
{
ShimRealcontext.Constructor = context => GenerateFakeContext();
ShimDbContext.AllInstances.Dispose = () => DummyDispose();
Service service = new Service();
int result = service.CountSomeEntites();
Assert.AreEqual(result, 2);
}
}
private FakeContext GenerateFakeContext()
{
FakeContext fakeContext = new FakeContext();
fakeContext.SomeEntities.AddRange(new[]
{
new SomeEntity {Id = 1, Name = "entity1"},
new SomeEntity {Id = 2, Name = "entity2"}
});
return fakeContext;
}
}
当我运行测试时,RealContext
构造函数被正确返回,在GenerateFakeContext()
方法中构建了一个FakeContext
,它包含2个SomeEntities
并返回,但紧接着,在服务中,变量 ctx
的 属性 SomeEntities
等于 null。
是不是因为我的变量ctx
被声明为new RealContext()
?但是调用 RealContext
returns 的构造函数一个 FakeContext()
,所以变量不应该是 FakeContext
类型吗?
我是不是做错了什么?或者有没有其他方法可以在不访问真实数据库的情况下测试服务?
我遇到了类似的情况,我通过构建配置和条件编译解决了它。这不是最好的解决方案,但它对我有用并解决了问题。这是收据:
1。创建 DataContext 接口
首先,您需要创建一个接口,该接口将由您要使用的两个上下文 classe 实现。让它命名为 'IMyDataContext'。在其中,您需要描述您需要访问的所有 DbSet。
public interaface IMyDataContext
{
DbSet<SomeEntity> SomeEntities { get; set; }
}
并且您的上下文 class 都需要推动它:
public partial class RealDataContext : DataContext, IMyDataContext
{
DbSet<SomeEntity> SomeEntities { get; set; }
/* Contructor, Initialization code, etc... */
}
public class FakeDataContext : DataContext, IMyDataContext
{
DbSet<SomeEntity> SomeEntities { get; set; }
/* Mocking, test doubles, etc... */
}
顺便说一句,您甚至可以在接口级别将属性设置为只读。
2。添加'Test'构建配置
Here 您可以找到如何添加新的构建配置。我将我的配置命名为 'Test'。创建新配置后,转到您的 DAL 项目属性,左窗格中的构建部分。在 'Configuration' 下拉列表中 select 您刚刚创建的配置,并在输入 'Conditional compilation symbols' 中输入 'TEST'.
3。封装上下文注入
需要说明的是,我的方法仍然是method/property基于DI的解决方案=)
所以现在我们需要实现一些注入代码。为简单起见,您可以将其直接添加到您的服务中,或者如果您需要更多抽象,则将其提取到另一个 class 中。主要思想是使用条件编译指令而不是 IoC 框架。
public class Service
{
// Injector method
private IMyDataContext GetContext() {
// Here is the main code
#if TEST // <-- In 'Test' configuration
// we will use fake context
return new FakeDataContext();
#else
// in any other case
// we will use real context
return new RealDataContext();
#endif
}
public int CountSomeEntites()
{
// the service works with interface and does know nothing
// about the implementation
using (IMyDataContext ctx = GetContext())
{
int result = ctx.SomeEntities.Count();
return result;
}
}
}
限制
所描述的方法解决了您所描述的问题,但它有一个局限性:由于 IoC 允许您在 运行 时间 动态 切换上下文,条件编译需要您重新编译 解决方案。
在我的情况下,这不是问题 - 我的代码没有被 100% 的测试覆盖,而且我没有在每个构建中 运行 它们。通常我 运行 只在提交代码之前进行测试,所以在 VS 中切换构建配置非常容易,运行 测试,确保没有任何问题,然后 return 进入调试模式。在发布模式下,您也不需要 运行 测试。即使您需要 - 您也可以创建 "Release build test mode" 配置并继续使用相同的解决方案。
另一个问题是如果您有连续集成 - 您需要对构建服务器进行额外设置。这里有两种方式:
- 设置两个构建定义:一个用于发布,一个用于测试。如果您的服务器设置为自动发布,您需要小心,因为测试失败将在部署第一个时显示在第二个中。
- 设置复杂的构建定义,第一次在测试配置中构建您的代码,运行s 测试,如果它们正常 - 然后重新编译目标配置中的代码并准备部署。
因此,作为任何解决方案,这个解决方案都是简单性和灵活性之间的另一种折衷。
更新
一段时间后,我明白了我上面描述的方式很重。我的意思是 - 构建配置。如果只有两个 IDataContext
实现:'Core' 和 'Fake' 你可以简单地使用 bool
参数和简单的 if/else
分支而不是编译指令 #if/#else/#endif
以及所有令人头疼的配置构建服务器的问题。
如果您有两个以上的实现 - 您可以使用枚举和 switch
块。这里的一个问题是定义在 default
情况下或值超出枚举范围时 return 的内容。
但这种方法的主要好处是您可以不再受限于编译时间。喷油器参数可以随时更改,例如使用 web.config 和 ConfigurationManager
。使用它,您可以在 运行 时间切换数据上下文。
我正在尝试测试服务查询中的业务逻辑。所以我不希望我的测试能够真正访问数据库,因为它们是单元测试,而不是集成测试。
所以我做了一个简单的例子来说明我的上下文以及我是如何尝试填充它的。
我有一个实体
public class SomeEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
和一项服务
public class Service
{
public int CountSomeEntites()
{
using (var ctx = new Realcontext())
{
int result = ctx.SomeEntities.Count();
return result;
}
}
}
这才是真正的背景
public partial class Realcontext : DbContext
{
public virtual DbSet<SomeEntity> SomeEntities { get; set; }
public Realcontext() : base("name=Realcontext")
{
InitializeContext();
}
partial void InitializeContext();
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
}
所以我尝试创建一个假上下文,并在我的测试方法中绕过了真实上下文的构造函数
这是假的上下文
public class FakeContext : DbContext
{
public DbSet<SomeEntity> SomeEntities { get; set; }
public FakeContext()
{
}
}
最后是测试class
[TestClass]
public class ServiceTests
{
[TestMethod]
public void CountEmployee_ShoulReturnCorrectResult()
{
using (ShimsContext.Create())
{
ShimRealcontext.Constructor = context => GenerateFakeContext();
ShimDbContext.AllInstances.Dispose = () => DummyDispose();
Service service = new Service();
int result = service.CountSomeEntites();
Assert.AreEqual(result, 2);
}
}
private FakeContext GenerateFakeContext()
{
FakeContext fakeContext = new FakeContext();
fakeContext.SomeEntities.AddRange(new[]
{
new SomeEntity {Id = 1, Name = "entity1"},
new SomeEntity {Id = 2, Name = "entity2"}
});
return fakeContext;
}
}
当我运行测试时,RealContext
构造函数被正确返回,在GenerateFakeContext()
方法中构建了一个FakeContext
,它包含2个SomeEntities
并返回,但紧接着,在服务中,变量 ctx
的 属性 SomeEntities
等于 null。
是不是因为我的变量ctx
被声明为new RealContext()
?但是调用 RealContext
returns 的构造函数一个 FakeContext()
,所以变量不应该是 FakeContext
类型吗?
我是不是做错了什么?或者有没有其他方法可以在不访问真实数据库的情况下测试服务?
我遇到了类似的情况,我通过构建配置和条件编译解决了它。这不是最好的解决方案,但它对我有用并解决了问题。这是收据:
1。创建 DataContext 接口
首先,您需要创建一个接口,该接口将由您要使用的两个上下文 classe 实现。让它命名为 'IMyDataContext'。在其中,您需要描述您需要访问的所有 DbSet。
public interaface IMyDataContext
{
DbSet<SomeEntity> SomeEntities { get; set; }
}
并且您的上下文 class 都需要推动它:
public partial class RealDataContext : DataContext, IMyDataContext
{
DbSet<SomeEntity> SomeEntities { get; set; }
/* Contructor, Initialization code, etc... */
}
public class FakeDataContext : DataContext, IMyDataContext
{
DbSet<SomeEntity> SomeEntities { get; set; }
/* Mocking, test doubles, etc... */
}
顺便说一句,您甚至可以在接口级别将属性设置为只读。
2。添加'Test'构建配置
Here 您可以找到如何添加新的构建配置。我将我的配置命名为 'Test'。创建新配置后,转到您的 DAL 项目属性,左窗格中的构建部分。在 'Configuration' 下拉列表中 select 您刚刚创建的配置,并在输入 'Conditional compilation symbols' 中输入 'TEST'.
3。封装上下文注入
需要说明的是,我的方法仍然是method/property基于DI的解决方案=)
所以现在我们需要实现一些注入代码。为简单起见,您可以将其直接添加到您的服务中,或者如果您需要更多抽象,则将其提取到另一个 class 中。主要思想是使用条件编译指令而不是 IoC 框架。
public class Service
{
// Injector method
private IMyDataContext GetContext() {
// Here is the main code
#if TEST // <-- In 'Test' configuration
// we will use fake context
return new FakeDataContext();
#else
// in any other case
// we will use real context
return new RealDataContext();
#endif
}
public int CountSomeEntites()
{
// the service works with interface and does know nothing
// about the implementation
using (IMyDataContext ctx = GetContext())
{
int result = ctx.SomeEntities.Count();
return result;
}
}
}
限制
所描述的方法解决了您所描述的问题,但它有一个局限性:由于 IoC 允许您在 运行 时间 动态 切换上下文,条件编译需要您重新编译 解决方案。
在我的情况下,这不是问题 - 我的代码没有被 100% 的测试覆盖,而且我没有在每个构建中 运行 它们。通常我 运行 只在提交代码之前进行测试,所以在 VS 中切换构建配置非常容易,运行 测试,确保没有任何问题,然后 return 进入调试模式。在发布模式下,您也不需要 运行 测试。即使您需要 - 您也可以创建 "Release build test mode" 配置并继续使用相同的解决方案。
另一个问题是如果您有连续集成 - 您需要对构建服务器进行额外设置。这里有两种方式:
- 设置两个构建定义:一个用于发布,一个用于测试。如果您的服务器设置为自动发布,您需要小心,因为测试失败将在部署第一个时显示在第二个中。
- 设置复杂的构建定义,第一次在测试配置中构建您的代码,运行s 测试,如果它们正常 - 然后重新编译目标配置中的代码并准备部署。
因此,作为任何解决方案,这个解决方案都是简单性和灵活性之间的另一种折衷。
更新
一段时间后,我明白了我上面描述的方式很重。我的意思是 - 构建配置。如果只有两个 IDataContext
实现:'Core' 和 'Fake' 你可以简单地使用 bool
参数和简单的 if/else
分支而不是编译指令 #if/#else/#endif
以及所有令人头疼的配置构建服务器的问题。
如果您有两个以上的实现 - 您可以使用枚举和 switch
块。这里的一个问题是定义在 default
情况下或值超出枚举范围时 return 的内容。
但这种方法的主要好处是您可以不再受限于编译时间。喷油器参数可以随时更改,例如使用 web.config 和 ConfigurationManager
。使用它,您可以在 运行 时间切换数据上下文。