使用Entity Framework的测试服务,无需依赖注入

时间:2016-05-17 14:16:48

标签: c# entity-framework unit-testing microsoft-fakes

我试图在服务中的查询中测试业务逻辑。所以我不希望我的测试能够真正访问数据库,因为它们是单元测试,而不是集成测试。

所以我做了一个关于我的背景的简单例子以及我是如何试图填补它的。

我有一个实体

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()
    {
    }
}

最后是测试类

[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构造函数被正确返回,FakeContext方法中构建GenerateFakeContext(),它包含2 SomeEntities并返回,但是在服务之后,变量SomeEntities的属性ctx等于null。

是因为我的变量ctx被声明为new RealContext()吗?但是调用RealContext的构造函数会返回FakeContext(),因此该变量应该是FakeContext类型的变量吗?

我做错了吗?或者有没有其他方法可以在不访问真实数据库的情况下测试服务?

1 个答案:

答案 0 :(得分:0)

我有simlair的情况,我用构建配置和条件编译解决了它。这不是最好的解决方案,但它对我有用并解决了问题。这是收据:

<强> 1。创建DataContext接口

首先,您需要创建一个接口,该接口将由您要使用的上下文classe实现。让它命名为'IMyDataContext'。在其中,您需要描述您需要访问的所有DbSet。

public interaface IMyDataContext
{
    DbSet<SomeEntity> SomeEntities { get; set; }
}

你的上下文类都需要阻止它:

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。添加“测试”构建配置

Here您可以找到如何添加新的构建配置。我把我的configuratin命名为'Test'。创建新配置后,转到左窗格中的DAL项目属性“构建”部分。在“配置”下拉列表中选择您刚刚创建的配置,并在输入“条件编译符号”中输入“TEST”。

第3。封装上下文注入

要清楚,我的方法仍然是基于方法/属性的DI解决方案=)

所以现在我们需要实现一些注入代码。为简单起见,您可以将其添加到服务中,或者如果需要更多抽象,则可以将其提取到另一个类中。主要思想是使用条件编译方向而不是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中切换构建配置非常容易,运行测试,确保没有任何损坏,然后返回调试模式。在发布模式下,您也不需要运行测试。即使您需要 - 您也可以“释放构建测试模式”配置并继续使用相同的解决方案。

另一个问题是如果你有连续集成 - 你需要对构建服务器进行额外的设置。这里有两种方法:

  • 设置两个构建定义:一个用于发布,另一个用于测试。如果您的服务器设置为自动释放,则需要小心,因为第一个部署时,测试失败将显示在第二个中。
  • 设置复杂的构建定义,这是第一次在Test配置中构建代码,运行test并且如果它们没问题 - 然后重新编译目标配置中的代码并准备部署。

因此,作为任何解决方案,这一点是简化和灵活性之间的又一折衷。

<强>更新

过了一段时间后,我才明白上面描述的方式非常沉重。我的意思是 - 构建配置。如果只有两个IDataContext实现:'Core'和'Fake',您只需使用bool参数和简单if/else分支而不是编译指令#if/#else/#endif和所有头部疼痛配置您的构建服务器。

如果您有两个以上的实现 - 您可以使用enum和switch块。这里的一个探讨是定义您将在default情况下返回的内容,或者值是否超出枚举范围。

但这种方法的主要好处是你不再受限于编译时间。可以随时更改Injector参数,例如使用web.config和ConfigurationManager。使用它,您可以在运行时切换数据上下文。