Autofixture: 生成具有复杂约束的对象

创建于 2018-08-26  ·  8评论  ·  资料来源: AutoFixture/AutoFixture

我试图找出使 AutoFixture 生成在构造函数中具有非平凡约束的对象的最佳方法。 例如,假设我想使用一个 PrimeNumber 数据结构,它接受一个 int 并且只接受素数。

在 AutoFixture 中生成这种结构实例的最佳方法是什么? 我的意思是,我显然会写一个自定义,但你会在那里放什么?

  • 你会生成随机整数并循环直到其中一个是素数(或者执行素数生成算法,当然)? 对于这种约束,这可能是可以接受的,但如果约束更难遵守,那将很快变得代价高昂。
  • 你会提供一些可接受值的有限列表吗?

此外,现在让我们说,我正在尝试创建一个实例,该实例采用几个理论上可以单独随机的参数,但这将在它们之间进行一些验证(例如,只有当 argB 时 argA 才能在这个值范围内是真的,并且 argC 必须根据 argA 值遵守不同的验证规则,或者 argC.X 属性必须与 argA.X 属性匹配,诸如此类)。

在这种情况下你会怎么做?

  • 一种定制来创建每种类型的有效实例(无需打扰任何外部验证),另一种定制将尝试创建大型复杂对象,循环直到创建有效实例?
  • 再次,提供一个有限可接受值的列表,这可能是可能性幅度的严重限制
  • 提供一个特殊的定制,只创建适合复杂对象验证的参数实例

最后(我可以创建几个问题,但我觉得所有这些主题都是同一问题的不同方面),每次我们添加一个新类时都必须创建和应用这种自定义,并且必须在任何时候维护这些自定义验证规则的更改似乎需要做很多工作,您是否应用了一些技术来缓解这种情况?

非常感谢,对不起,我希望帖子不要太乱。

question

最有用的评论

再会! 最后我分配了一些类型来回答 - 抱歉迟来的回复😊

首先要注意 AutoFixture 的核心非常简单,我们没有对带有约束的复杂树的内置支持。 简而言之,创建策略如下:

  • 查找公共构造函数或静态工厂方法(返回当前类型实例的静态方法)。
  • 解析构造函数参数并激活实例。
  • 使用生成的值填充可写的公共属性和字段。

使用当前方法,正如您之前发现的那样,您无法以某种方式控制依赖项约束。

我们有一些自定义点来指定如何构建特定类型,但它们相对简单,不支持那些复杂的规则。

在 AutoFixture 中生成这种结构实例的最佳方法是什么? 我的意思是,我显然会写一个自定义,但你会在那里放什么?

  • 你会生成随机整数并循环直到其中一个是素数(或者执行素数生成算法,当然)? 对于这种约束,这可能是可以接受的,但如果约束更难遵守,那将很快变得代价高昂。

  • 你会提供一些可接受值的有限列表吗?

好吧,不幸的是,我在这里没有看到灵丹妙药,方法取决于情况。 如果您不依赖于过于随机的值,或者单个 SUT 仅消耗 1-2 个素数,那么硬编码素数并从中挑选可能没问题(我们有ElementsBulider<>内置助手对于那些情况)。 另一方面,如果您需要大量素数并且您使用长素数序列进行操作,那么最好编写一个算法来动态生成它们。

此外,现在让我们说,我正在尝试创建一个实例,该实例采用几个理论上可以单独随机的参数,但这将在它们之间进行一些验证(例如,只有当 argB 时 argA 才能在这个值范围内是真的,并且 argC 必须根据 argA 值遵守不同的验证规则,或者 argC.X 属性必须与 argA.X 属性匹配,诸如此类)。

在这种情况下你会怎么做?

真的是一个很好的问题,不幸的是 AutoFixture 不允许以开箱即用的方式解决它。 通常我试图隔离每种类型的自定义,因此只为一种类型控制创建一种类型的自定义。 但在我的情况下,类型是独立的,显然在你的情况下它不会很好地工作。 此外,AutoFixture 不提供开箱即用的上下文,因此当您为特定类型编写自定义时,您无法清楚地了解您在其中创建对象(内部称为样本)的上下文。

最重要的是,我会说我通常会推荐以下策略:

  • 尝试以仅控制单个对象类型创建的方式为每种类型创建自定义。
  • 如果您需要创建具有特定约束的依赖项,最好在自定义中也激活这些依赖项。 如果您的依赖项是可变的,您可以要求 AutoFixture 为您创建依赖项,然后以兼容的方式对其进行配置。

这样你就不会过多地与内部架构相矛盾,而且它是如何工作的就会很清楚。 当然,这种方式可能非常冗长。

如果具有复杂约束的情况并不常见,那么现有功能可能对您来说就足够了。 但是如果你的领域模型真的充满了这种情况,坦率地说 AutoFixture 可能不是你的最佳工具。 可能市场上有更好的工具可以以最优雅的方式解决此类问题。 当然,值得一提的是,AutoFixture 非常灵活,您几乎可以覆盖所有内容,因此您始终可以在 AutoFixture 核心之上创建自己的 DSL……但是您应该评估哪种方式对您来说更便宜😉

我们也问问@ploeh的想法。 通常Mark的回答很深刻,他会先找出根本原因,而不是解决后果😅

如果您有更多问题 - 请提问! 我将永远欢迎回答他们。

PS FWIW,我决定为您提供一个示例,在那里我尝试使用 AutoFixture 并解决类似的问题(我试图保持简单,但在您的情况下可能无法完全工作):


点击查看源代码

```c#
使用系统;
使用 AutoFixture;
使用 AutoFixture.Xunit2;
使用 Xunit;

命名空间 AutoFixturePlayground
{
公共静态类 Util
{
public static bool IsPrime(int number)
{
// 复制自https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
``

所有8条评论

很抱歉无线电沉默。 我们还活着,我会尽快回复 - 这些天我的主要工作非常忙。 也在开发 NSubstitute v4 版本,所以时间非常有限资源:沉思:这个问题很难,所以在发布答案之前想考虑所有可能的方法。

感谢您的耐心等待,保持曲调:wink:

你好,
有什么消息吗?
没有压力(我知道练习 😄 ,而且它并没有真正阻塞,我真的很喜欢一些受过教育的建议),只是想知道您是否有一些知名度。
非常感谢!

再会! 最后我分配了一些类型来回答 - 抱歉迟来的回复😊

首先要注意 AutoFixture 的核心非常简单,我们没有对带有约束的复杂树的内置支持。 简而言之,创建策略如下:

  • 查找公共构造函数或静态工厂方法(返回当前类型实例的静态方法)。
  • 解析构造函数参数并激活实例。
  • 使用生成的值填充可写的公共属性和字段。

使用当前方法,正如您之前发现的那样,您无法以某种方式控制依赖项约束。

我们有一些自定义点来指定如何构建特定类型,但它们相对简单,不支持那些复杂的规则。

在 AutoFixture 中生成这种结构实例的最佳方法是什么? 我的意思是,我显然会写一个自定义,但你会在那里放什么?

  • 你会生成随机整数并循环直到其中一个是素数(或者执行素数生成算法,当然)? 对于这种约束,这可能是可以接受的,但如果约束更难遵守,那将很快变得代价高昂。

  • 你会提供一些可接受值的有限列表吗?

好吧,不幸的是,我在这里没有看到灵丹妙药,方法取决于情况。 如果您不依赖于过于随机的值,或者单个 SUT 仅消耗 1-2 个素数,那么硬编码素数并从中挑选可能没问题(我们有ElementsBulider<>内置助手对于那些情况)。 另一方面,如果您需要大量素数并且您使用长素数序列进行操作,那么最好编写一个算法来动态生成它们。

此外,现在让我们说,我正在尝试创建一个实例,该实例采用几个理论上可以单独随机的参数,但这将在它们之间进行一些验证(例如,只有当 argB 时 argA 才能在这个值范围内是真的,并且 argC 必须根据 argA 值遵守不同的验证规则,或者 argC.X 属性必须与 argA.X 属性匹配,诸如此类)。

在这种情况下你会怎么做?

真的是一个很好的问题,不幸的是 AutoFixture 不允许以开箱即用的方式解决它。 通常我试图隔离每种类型的自定义,因此只为一种类型控制创建一种类型的自定义。 但在我的情况下,类型是独立的,显然在你的情况下它不会很好地工作。 此外,AutoFixture 不提供开箱即用的上下文,因此当您为特定类型编写自定义时,您无法清楚地了解您在其中创建对象(内部称为样本)的上下文。

最重要的是,我会说我通常会推荐以下策略:

  • 尝试以仅控制单个对象类型创建的方式为每种类型创建自定义。
  • 如果您需要创建具有特定约束的依赖项,最好在自定义中也激活这些依赖项。 如果您的依赖项是可变的,您可以要求 AutoFixture 为您创建依赖项,然后以兼容的方式对其进行配置。

这样你就不会过多地与内部架构相矛盾,而且它是如何工作的就会很清楚。 当然,这种方式可能非常冗长。

如果具有复杂约束的情况并不常见,那么现有功能可能对您来说就足够了。 但是如果你的领域模型真的充满了这种情况,坦率地说 AutoFixture 可能不是你的最佳工具。 可能市场上有更好的工具可以以最优雅的方式解决此类问题。 当然,值得一提的是,AutoFixture 非常灵活,您几乎可以覆盖所有内容,因此您始终可以在 AutoFixture 核心之上创建自己的 DSL……但是您应该评估哪种方式对您来说更便宜😉

我们也问问@ploeh的想法。 通常Mark的回答很深刻,他会先找出根本原因,而不是解决后果😅

如果您有更多问题 - 请提问! 我将永远欢迎回答他们。

PS FWIW,我决定为您提供一个示例,在那里我尝试使用 AutoFixture 并解决类似的问题(我试图保持简单,但在您的情况下可能无法完全工作):


点击查看源代码

```c#
使用系统;
使用 AutoFixture;
使用 AutoFixture.Xunit2;
使用 Xunit;

命名空间 AutoFixturePlayground
{
公共静态类 Util
{
public static bool IsPrime(int number)
{
// 复制自https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
``

@zvirja

哇,谢谢你的详细回答,真的很有趣。 我将不得不做一些测试并估计哪些值得做或不值得做,但总而言之,这很棒。

我不认为我有太多的依赖需要处理,所以你的方法可能是一个很好的方法。 当然,如果@ploeh碰巧有什么要补充的,我会很荣幸👌

再次感谢,继续努力!

我对 AutoFixture 和基于属性的测试的经验是,基本上有两种方法可以解决这些问题:

  • 过滤
  • 算法创建

(在我写的时候,我的直觉表明这些可能分别是 _catamorphisms_ 和 _anamorphisms_,但我必须更多地考虑这一点,所以除此之外,这主要是给我自己的一个注释。)

如果 _most_ 随机生成的值适合任何必须满足的约束,那么使用现有的生成器,但丢弃偶尔不合适的值,可能是解决问题的最简单方法。

另一方面,如果过滤器意味着丢弃大多数随机数据,则您必须提出一种算法,该算法可能基于随机种子值,生成符合相关约束的值。

几年前,我做了一个演讲,展示了 FsCheck 上下文中这两种方法的一些简单示例。 该演示文稿实际上是采用相同方法的演讲的演变,但只是使用 AutoFixture。 不幸的是,该谈话没有录音。

可以通过两种方式解决质数要求。

过滤方法是生成不受约束的数字,然后丢弃数字,直到得到一个确实是素数的数字。

算法方法是使用像素数筛这样的算法来生成素数。 然而,这不是随机的,所以人们可能想弄清楚如何随机化它。

一旦其他人开始查看该库,几乎立即就出现了如何处理 AutoFixture 中的约束值的总体问题,当时我写了一篇文章,我仍然参考: http :

关于相互关联的多个值的问题,我不想给出任何一般性指导。 这类问题通常是 XY 问题。 在许多情况下,一旦我了解了细节,替代设计不仅可以解决 AutoFixture 的问题,还可以解决生产代码库本身的问题。

尽管如此,即使存在 XY 问题,仍然会存在这样的情况,这可能是一个合理的问题,但我宁愿逐案处理这些问题,因为根据我的经验,它们是稀有的。

因此,如果您有这方面的具体示例,我可能会提供帮助,但我认为我无法有意义地回答一般性问题。

@ploeh 非常感谢这个答案,这证实了我正在考虑的方法(你让我对 cata- 和 anamorphisms 感到好奇😃)。
我完全同意相互依赖的值主要是 XY 问题(至少在我的情况下),问题是在处理遗留(未经测试的😢)代码时,处理这些值是编写一些测试的良好开端,直到我们得到是时候正确重构它了。

无论如何,你的两个答案都很好地解决了这个问题,我想我很高兴从那里开始。
谢谢!

顺便说一句,我忘了提到我的回答只是对@zvirja的补充。 那里已经是很好的答案了👍

我没有采取任何其他方式😄

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

zvirja picture zvirja  ·  8评论

ecampidoglio picture ecampidoglio  ·  7评论

joelleortiz picture joelleortiz  ·  4评论

Accc99 picture Accc99  ·  4评论

zvirja picture zvirja  ·  4评论