在MS单元测试

时间:2015-07-14 00:09:11

标签: entity-framework entity-framework-6 mstest

我不确定如何将EF融入我的业务逻辑测试中。让我举一个例子说明它在运行时是如何工作的(没有测试,常规应用程序运行):

Context.Set<T>.Add(instance);

当我使用上面的泛型方法添加实体时,会将一个实例添加到上下文中,EF会修复幕后的所有导航属性。例如,如果存在[instance.Parent]属性和[parent.Instances]集合属性(1对多关系),EF将自动将实例添加到幕后的parent.Instances集合中。

我的代码取决于[parent.Instances]集合,如果它是空的,它将失败。当我使用MS测试框架编写单元测试时,如何重用EF的功能,所以它仍然可以完成幕后工作,但是将内存作为数据存储而不是实际的数据库?我对EF是否成功添加,修改或删除了数据库中的某些内容并不感兴趣,我只是想在内存集中获得EF魔法。

2 个答案:

答案 0 :(得分:3)

我一直在使用我创建的模拟DbContext和模拟DbSet。它们将测试数据存储在内存中,并允许您执行可在DbSet上执行的大多数标准操作。

最初获取DbContext的代码必须进行更改,以便在单元测试下运行时获取MockDbContext。您可以使用以下代码确定您是否在MSTest下运行:

   { field_a: abc,
     field_b: def,
     sub_docs:
        [ { score: 1 },
          { score: 1 },
          { score: 1 } ] }

以下是MockDbContext的代码:

public static bool IsInUnitTest
{
    get
    {
        return AppDomain.CurrentDomain.GetAssemblies()
            .Any(assembly =>
                 assembly.FullName.StartsWith(
                     "Microsoft.VisualStudio.QualityTools.UnitTestFramework"));
    }
}

这是MockDbSet的代码:

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication5
{
    // ProductionDbContext would be DbContext class
    // generated by Entity Framework
    public class MockDbContext: ProductionDbContext
    {
        public MockDbContext()
        {
            LoadFakeData();
        }

        // Entities (for which we'll provide MockDbSet implementation 
        // and test data)
        public override DbSet<Account> Accounts { get; set; }
        public override DbSet<AccountGenLink> AccountGenLinks { get; set; }
        public override DbSet<AccountPermit> AccountPermits { get; set; }
        public override DbSet<AcctDocGenLink> AcctDocGenLinks { get; set; }

        // DbContext method overrides
        private int InternalSaveChanges()
        {
            // Just return 0 in the mock
            return 0;
        }

        public override int SaveChanges()
        {
            return InternalSaveChanges();
        }

        public override Task<int> SaveChangesAsync()
        {
            return Task.FromResult(InternalSaveChanges());
        }

        public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
        {
            // Just ignore the cancellation token in the mock
            return SaveChangesAsync();
        }

        private void LoadFakeData()
        {
            // Tables
            Accounts = new MockDbSet<Account>(this);
            Accounts.AddRange(new List<Account>
            {
                new Account
                {
                    SSN_EIN = "123456789", CODE = "A", accttype = "CD", 
                    acctnumber = "1", pending = false, BankOfficer1 = string.Empty, 
                    BankOfficer2 = null, Branch = 0, type = "18", drm_rate_code = "18", 
                    officer_code = string.Empty, open_date = new DateTime(2010, 6, 8), 
                    maturity_date = new DateTime(2010, 11, 8), HostAcctActive = true, 
                    EffectiveAcctStatus = "A"
                },
                new Account
                {
                    SSN_EIN = "123456789", CODE = "A", accttype = "DD", 
                    acctnumber = "00001234", pending = false, BankOfficer1 = "BCK", 
                    BankOfficer2 = string.Empty, Branch = 0, type = "05", drm_rate_code = "00", 
                    officer_code = "DJT", open_date = new DateTime(1998, 9, 14), 
                    maturity_date = null, HostAcctActive = true, 
                    EffectiveAcctStatus = "A"
                },
                new Account
                {
                    SSN_EIN = "123456789", CODE = "A", accttype = "LN", acctnumber = "1", 
                    pending = false, BankOfficer1 = "LMP", BankOfficer2 = string.Empty, 
                    Branch = 0, type = "7", drm_rate_code = null, officer_code = string.Empty,
                    open_date = new DateTime(2001, 10, 24), 
                    maturity_date = new DateTime(2008, 5, 2), HostAcctActive = true, 
                    EffectiveAcctStatus = "A"
                }
            });

            AccountGenLinks = new MockDbSet<AccountGenLink>(this);
            AccountGenLinks.AddRange(new List<AccountGenLink>
            {
                // Add your test data here if needed
            });

            AccountPermits = new MockDbSet<AccountPermit>(this);
            AccountPermits.AddRange(new List<AccountPermit>
            {
                // Add your test data here if needed
            });

            AcctDocLinks = new MockDbSet<AcctDocLink>(this);
            AcctDocLinks.AddRange(new List<AcctDocLink>
            {
                new AcctDocLink { ID = 1, SSN_EIN = "123456789", CODE = "A", accttype = "DD", 
                                  acctnumber = "00001234", DocID = 50, DocType = 5 },
                new AcctDocLink { ID = 25, SSN_EIN = "123456789", CODE = "6", accttype = "CD", 
                                  acctnumber = "1", DocID = 6750, DocType = 5 }
            });
        }
    }
}

答案 1 :(得分:0)

如果您使用EntityFramework-Reverse-POCO-Code-First-Generator from Simon Hughes及其生成的FakeContext,则通过一些调整仍然可以使用Jeff Prince的方法。此处的微小区别是我们使用了部分类支持,并在FakeContext和FakeDbSet中实现了InitializePartial()方法。更大的区别在于,反向POCO FakeContext不能从DbContext继承,因此我们无法轻松地使MetadataWorkspace知道哪些列是标识。答案是使用虚假的连接字符串创建“真实”上下文,并使用该上下文获取FakeDbSet的EntitySetBase。应当将其粘贴到新源文件的适当名称空间中,重命名上下文,并且在项目的其余部分中无需执行任何其他操作。

/// <summary>   
/// This code will set Identity columns to be unique. It behaves differently from the real context in that the
/// identities are generated on add, not save. This is inspired by https://stackoverflow.com/a/31795273/1185620 and
/// modified for use with the FakeDbSet and FakeContext that can be generated by EntityFramework-Reverse-POCO-Code-
/// First-Generator from Simon Hughes.
/// 
/// Aside from changing the name of the FakeContext and the type used to in its InitializePartial() as
/// the 'realContext' this file can be pasted into another namespace for a completely unrelated context. If you
/// have additional implementation for the InitializePartial methods in the FakeContext or FakeDbSet, change the
/// name to InitializePartial2 and they will be called after InitializePartial is called here. Please don't add
/// code unrelated to the above purpose to this file - make another file to further extend the partial class. 
/// </summary>
partial class FakeFooBarBazContext
{
    /// <summary>   Initialization of FakeContext to handle setting an identity for columns marked as
    ///             <c>IsStoreGeneratedIdentity</c> when an item is Added to the DbSet. If this signature
    ///             conflicts with another partial class, change that signature to implement 
    ///             <see cref="InitializePartial2"/>, as that will be called when this is complete. </summary>
    partial void InitializePartial()
    {
        // Here we need to get a 'real' ObjectContext so we can get the metadata for determining
        // identity columns. Since FakeContext doesn't inherit from DbContext, create 
        // the real one with a bogus connection string.
        using (var realContext = new FooBarBazContext("Server=."))
        {
            var objectContext = (realContext as IObjectContextAdapter).ObjectContext;

            // Reflect over the public properties that return DbSet<> and get it. If it is 
            // of type FakeDbSet<>, call InitializeWithContext() on it.
            var fakeDbSetGenericType = typeof(FakeDbSet<>);
            var dbSetGenericType = typeof(DbSet<>);
            var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (var prop in properties)
            {
                if (!prop.PropertyType.IsGenericType || prop.GetMethod == null)
                    continue;
                if (prop.PropertyType.GetGenericTypeDefinition() != dbSetGenericType)
                    continue;
                var dbSetObj = prop.GetMethod.Invoke(this, null);
                var dbSetObjType = dbSetObj?.GetType();
                if (dbSetObjType?.GetGenericTypeDefinition() != fakeDbSetGenericType)
                    continue;

                var initMethod = dbSetObjType.GetMethod(nameof(FakeDbSet<object>.InitializeWithContext),
                    BindingFlags.NonPublic | BindingFlags.Instance,
                    null, new[] {typeof(ObjectContext)}, new ParameterModifier[] { });
                initMethod.Invoke(dbSetObj, new object[] {objectContext});
            }
        }

        InitializePartial2();
    }

    partial void InitializePartial2();
}

partial class FakeDbSet<TEntity>
{
    private EntitySetBase EntitySet { get; set; }

    /// <summary>   Initialization of FakeDbSet to handle setting an identity for columns marked as
    ///             <c>IsStoreGeneratedIdentity</c> when an item is Added to the DbSet. If this signature
    ///             conflicts with another partial class, change that signature to implement 
    ///             <see cref="InitializePartial2"/>, as that will be called when this is complete. </summary>
    partial void InitializePartial()
    {
        // The only way we know something was added to the DbSet from this partial class
        // is to hook the CollectionChanged event.
        _data.CollectionChanged += DataOnCollectionChanged;

        InitializePartial2();
    }

    internal void InitializeWithContext(ObjectContext objectContext)
    {
        // Get entity set for entity. Used when we figure out whether to generate IDENTITY values
        EntitySet = objectContext
            .MetadataWorkspace
            .GetItems<EntityContainer>(DataSpace.SSpace).First()
            .BaseEntitySets
            .FirstOrDefault(item => item.Name == typeof(TEntity).Name);
    }

    private void DataOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action != NotifyCollectionChangedAction.Add)
            return;
        foreach (TEntity entity in e.NewItems)
            GenerateIdentityColumnValues(entity);
    }

    /// <summary>   The purpose of this method, which is called after a row is added, is to ensure that Identity column values are
    ///             properly initialized. If this was a real Entity Framework, this task would be handled by the data provider
    ///             when SaveChanges[Async]() is called and the value(s) would then be propagated back into the entity.  
    ///             In the case of FakeDbSet, there is nothing that will do that, so we have to make this at-least token effort 
    ///             to ensure the columns are properly initialized, even if it is done at the incorrect time.
    /// </summary>
    private void GenerateIdentityColumnValues(TEntity entity)
    {
        foreach (var member in EntitySet.ElementType.Members)
        {
            if (!member.IsStoreGeneratedIdentity)
                continue;

            foreach (var metadataProperty in member.TypeUsage.EdmType.MetadataProperties)
            {
                if (metadataProperty.Name != "PrimitiveTypeKind")
                    continue;

                var entityProperty = entity.GetType().GetProperty(member.Name);
                var identityColumnGetter = entityProperty.GetGetMethod();

                // Note that we'll get the current value of the column and, 
                // if it is nonzero, we'll leave it alone.  We do this because 
                // the test data in our mock DbContext provides values for the 
                // Identity columns and many of those values are foreign keys 
                // in other entities (where we also provide test data).  We don't
                // want to disturb any existing relationships defined in the test data.
                bool isDefaultForType;
                var columnType = (PrimitiveTypeKind)metadataProperty.Value;
                switch (columnType)
                {
                    case PrimitiveTypeKind.SByte:
                        isDefaultForType = default(SByte) == (SByte)identityColumnGetter.Invoke(entity, null);
                        break;
                    case PrimitiveTypeKind.Int16:
                        isDefaultForType = default(Int16) == (Int16)identityColumnGetter.Invoke(entity, null);
                        break;
                    case PrimitiveTypeKind.Int32:
                        isDefaultForType = default(Int32) == (Int32)identityColumnGetter.Invoke(entity, null);
                        break;
                    case PrimitiveTypeKind.Int64:
                        isDefaultForType = default(Int64) == (Int64)identityColumnGetter.Invoke(entity, null);
                        break;
                    case PrimitiveTypeKind.Decimal:
                        isDefaultForType = default(Decimal) == (Decimal)identityColumnGetter.Invoke(entity, null);
                        break;
                    default:
                        // In SQL Server, an Identity column can be of one of the following data types:
                        // tinyint (SqlByte, byte), smallint (SqlInt16, Int16), int (SqlInt32, Int32), 
                        // bigint (SqlInt64, Int64), decimal (with a scale of 0) (SqlDecimal, Decimal),
                        // or numeric (with a scale of 0) (SqlDecimal, Decimal). Those are handled above.
                        // 'If we don't know, we throw'
                        throw new InvalidOperationException($"Unsupported Identity Column Type {columnType}");
                }

                // From this point on, we can return from the method, as only one identity column is 
                // possible per table and we found it.
                if (!isDefaultForType)
                    return; 

                var identityColumnSetter = entityProperty.GetSetMethod();
                lock (Local)
                {
                    switch (columnType)
                    {
                        case PrimitiveTypeKind.SByte:
                        {
                            SByte maxExistingColumnValue = 0;
                            foreach (var item in Local.ToList())
                                maxExistingColumnValue = Math.Max(maxExistingColumnValue, (SByte) identityColumnGetter.Invoke(item, null));
                            identityColumnSetter.Invoke(entity, new object[] {(SByte) (++maxExistingColumnValue)});
                            return;
                        }
                        case PrimitiveTypeKind.Int16:
                        {
                            Int16 maxExistingColumnValue = 0;
                            foreach (var item in Local.ToList())
                                maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int16) identityColumnGetter.Invoke(item, null));
                            identityColumnSetter.Invoke(entity, new object[] {(Int16) (++maxExistingColumnValue)});
                            return;
                        }
                        case PrimitiveTypeKind.Int32:
                        {
                            Int32 maxExistingColumnValue = 0;
                            foreach (var item in Local.ToList())
                                maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int32) identityColumnGetter.Invoke(item, null));
                            identityColumnSetter.Invoke(entity, new object[] {(Int32) (++maxExistingColumnValue)});
                            return;
                        }
                        case PrimitiveTypeKind.Int64:
                        {
                            Int64 maxExistingColumnValue = 0;
                            foreach (var item in Local.ToList())
                                maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int64) identityColumnGetter.Invoke(item, null));
                            identityColumnSetter.Invoke(entity, new object[] {(Int64) (++maxExistingColumnValue)});
                            return;
                        }
                        case PrimitiveTypeKind.Decimal:
                        {
                            Decimal maxExistingColumnValue = 0;
                            foreach (var item in Local.ToList())
                                maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Decimal) identityColumnGetter.Invoke(item, null));
                            identityColumnSetter.Invoke(entity, new object[] {(Decimal) (++maxExistingColumnValue)});
                            return;
                        }
                    }
                }
            }
        }
    }

    partial void InitializePartial2();
}