【问题标题】:How would you map a derived class to its base class in Entity Framework?您如何将派生类映射到实体框架中的基类?
【发布时间】:2021-06-28 17:53:04
【问题描述】:

我正在尝试为实体框架项目中的数据库构建集成测试模式,但在寻找接缝时遇到了一些挑战。假设我有一个模型,它可能具有复杂的属性...

public class Sample {
    public string Name { get; set; }
    /* many other complex / required properties */
}

我的典型测试模式会遵循 AAA:

public void Test() {
    // Arrange
    var sample1 = new Sample() { Name = "Test1", /* ... */ };
    var sample2 = new Sample() { Name = "Test2", /* ... */ };
    _context.Set<Sample>().AddRange(sample1, sample2);
    _context.SubmitChanges();

    // Act
    var results = _systemUnderTest.Act();

    // Assert
    Assert.IsNotNull(results)
}

由于实体对象非常复杂,配置所有实体所需的字段是一项艰巨的任务。通常,所有这些字段都不需要是测试的特定值。所以我想采用一种使用派生类和构造函数的模式,这样我就可以使用对象初始化器并删除很多样板。

public class SampleTest() : Sample {
    public SampleTest() { 
        Name = "Test";
        /* other default values and complex initialization */
    }
}

public void Test() {
    var sample1 = new TestSample();
    var sample2 = new TestSample { Name = "Custom Name for Test" };
    _context.Set<Sample>().AddRange(sample1, sample2);
    _context.SaveChanges();

    /* snip */
}

但是,当我这样做时,我收到错误'Mapping and metadata information could not be found for EntityType 'TestSample'. 我理解错误消息,但我希望有一种方法可以选择退出或绕过它。我想确定一个接缝,我可以指示 EF 根据其基类处理所有派生的“测试”类。

我尝试忽略 OnModelCreating 方法中不起作用的类型。我考虑过创建一个自定义 EntityTypeConfiguration ,其中包括 SampleTest 的映射,但我还没有找到一种方法来映射回没有鉴别器的同一个表。我在这里考虑过 EF 约定,但我不确定这是否能解决问题。

我可以尝试使用方法而不是派生类来实现它,类似于下面的代码,但我不觉得那样优雅或可维护。我不想沿着这些思路探索选项。

public class Factory {
    public static Sample CreateSample() {
        return new Sample() { Name = "Test1" };
    } 
}

public void Test() {
    var sample1 = Factory.Create<Sample>();
    var sample2 = Factory.Create<Sample>();
    sample2.Name == "Custom Name for Test";
}

任何帮助将不胜感激。

【问题讨论】:

  • EF really 不能很好地处理继承 - 这是the Object-Relational-Impedance-Mismatch problem 的核心,也是为什么这么多 ORM 表面上看起来不错,但超出简单的 1:1 的原因类/表设计。现在,您使用的是数据库优先还是代码优先?你在使用迁移吗?
  • “因为实体对象非常复杂,所以配置所有实体所需的字段是一项艰巨的任务” - 您实际上并没有在您发布的代码中“配置任何字段”,请执行你的意思是定义测试数据?你知道有一些工具可以为你做到这一点,比如 Mockaroo?
  • @dai - 我们使用代码优先,没有迁移。没错,我会定义测试数据。

标签: .net automated-tests entity-framework-6


【解决方案1】:

你想太多了,因此浪费了精力。您不应该简单地使用继承来为您的测试创建具有预定义属性值的对象,而应使用静态工厂方法。

而不是这个:

public class SampleTest() : Sample {
    public SampleTest() { 
        Name = "Test";
        /* other default values and complex initialization */
    }
}

public void Test() {
    var sample1 = new TestSample();
    var sample2 = new TestSample { Name = "Custom Name for Test" };
    _context.Set<Sample>().AddRange(sample1, sample2);
    _context.SaveChanges();

    /* snip */
}

这样做:

static Sample CreateTestSample( ref Int32 id, String testName )
{
    return new Sample()
    {
        SampleId = id++, // Assuming this is your PK property
        Name = "Custom Name for " + testName
    };
}

public void TestMethod1()
{
    Int32 id = 1;
    var sample1 = CreateTestSample( ref id, testName: nameof(TestMethod1) );
    var sample2 = CreateTestSample( ref id, testName: nameof(TestMethod1) );
    _context.Set<Sample>().AddRange( sample1, sample2 );
    _context.SaveChanges();

    /* snip */
}

public void TestMethod2()
{
    Int32 id = 1;
    var sample1 = CreateTestSample( ref id, testName: nameof(TestMethod2) );
    var sample2 = CreateTestSample( ref id, testName: nameof(TestMethod2) );
    var sample2 = CreateTestSample( ref id, testName: nameof(TestMethod2) );
    _context.Set<Sample>().AddRange( sample1, sample2 );
    _context.SaveChanges();

    /* snip */
}

您还可以使用可选参数在适当的情况下更轻松地覆盖示例数据默认值,例如,假设您有一个实体 PersonPersonIdNameDoBStackOverflowScore.. .

static Person CreateTestPerson(
    ref Int32 id,
    String name = null,
    DateTime? dob = null,
    Int32? stackOverflowScore = null
)
{
    return new Person()
    {
        PersonId           = id++,
        Name               = name ?? "John Smith",
        DoB                = dob ?? DateTime.UtcNow,
        StackOverflowScore = stackOverflowScore
    };
}

public void TestMethod1()
{
    Int32 id = 1;
    Person p1 = CreateTestPerson( ref id, name: "Matt", stackOverflowScore: 2223 );
    Person p2 = CreateTestPerson( ref id, dob: new DateTime( 1901, 1, 1 ) );

    _context.People.Attach( p1 );
    _context.People.Attach( p2 );

    _context.SaveChanges();

    /* snip */
}

【讨论】:

    【解决方案2】:

    测试需要简单,否则您可能会花费与调试被测系统一样多的时间来调试测试。我会避免为此需要创建类层次结构。

    只要写一个方法。

    static class Utilities
    {
        public static Sample WithDefaults(this Sample sample)
        {
            sample.OtherField1 = "Sample value 1";
            sample.OtherField2 = "Sample value 2";
            sample.OtherField3 = "Sample value 3";
            return sample;
        }
    }
    
    
    public void Test() {
        // Arrange
        var sample1 = new Sample { Name = "Test1"}.WithDefaults();
        var sample2 = new Sample { Name = "Test2"}.WithDefaults();
        _context.Set<Sample>().AddRange(sample1, sample2);
        _context.SubmitChanges();
    
        // Act
        var results = _systemUnderTest.Act();
    
        // Assert
        Assert.IsNotNull(results)
    }
    

    【讨论】:

    • 在此解决方案中,'WithDefaults()' 将在对象创建后运行,并将覆盖可能已设置的任何必填字段。例如new Sample { OtherField1 = "test value" } 这会导致扩展方法的复杂性。我觉得这会很脆弱。
    • 实例化一个对象并在之后设置它的属性是很常见的,我很确定您的工程师可以设法使用这样的代码。我很难想象你的想法,坦率地说,这很奇怪。为了完全理解代码,开发人员必须在原始构造函数、新构造函数和测试代码之间切换。只写一个方法就不容易出错。至于“优雅”,我认为单元测试并不意味着优雅。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-06
    • 1970-01-01
    相关资源
    最近更新 更多