Autofixture: Создать объект со сложными ограничениями

Созданный на 26 авг. 2018  ·  8Комментарии  ·  Источник: AutoFixture/AutoFixture

Я пытаюсь найти лучший способ заставить AutoFixture сгенерировать объект, который имел бы нетривиальные ограничения в конструкторе. Например, предположим, что я хочу использовать структуру данных PrimeNumber, которая будет принимать int и принимать только простые числа.

Как лучше всего создать экземпляр такой структуры в AutoFixture? Я имею в виду, что я, очевидно, напишу настройку, но что бы вы туда добавили?

  • Вы бы сгенерировали случайные числа и цикл до тех пор, пока одно из них не станет простым (или, конечно, выполните алгоритм генерации простых чисел)? Это могло быть приемлемо для такого рода ограничений, но если бы ограничение было более сложным для соблюдения, это быстро стало бы дорогостоящим.
  • Не могли бы вы предоставить конечный список некоторых приемлемых значений?

Более того, предположим теперь, что я пытаюсь создать экземпляр чего-то, что принимает несколько аргументов, которые теоретически могут быть случайными по отдельности, но при этом будет выполняться некоторая проверка между ними (например, argA может находиться в этом диапазоне значений, только если argB истинно, и argC должен соответствовать различным правилам проверки в зависимости от значения argA, или свойство argC.X должно совпадать со свойством argA.X, что-то в этом роде).

Что бы вы сделали в этом случае?

  • Одна настройка для создания действительного экземпляра каждого типа (не беспокоясь о какой-либо внешней проверке), а другая, которая будет пытаться создать большой сложный объект, зацикливаясь до тех пор, пока не будет создан действительный экземпляр?
  • Опять же, предоставьте список конечных допустимых значений, которые могут быть серьезным ограничением амплитуды возможностей.
  • Предоставьте специальную настройку, которая создаст только экземпляры аргументов, которые подходят для проверки сложного объекта.

И, наконец (я мог бы создать несколько проблем, но я чувствовал, что все эти предметы представляют собой разные аспекты одной и той же проблемы), необходимость создавать и применять такие настройки каждый раз, когда мы добавляем новый класс, и необходимость поддерживать эти настройки всякий раз, когда изменение правил валидации требует много работы. Применяете ли вы какие-нибудь приемы, чтобы это смягчить?

Большое спасибо, извините за длинный и, надеюсь, не слишком грязный пост.

question

Самый полезный комментарий

Добрый день! Наконец, я выделил немного текста для ответа - извините за запоздалый ответ 😊

Прежде всего обратите внимание, что ядро ​​AutoFixture довольно простое, и у нас нет встроенной поддержки сложных деревьев с ограничениями. Вкратце, стратегия создания выглядит следующим образом:

  • Найдите общедоступный конструктор или статический фабричный метод (статический метод, возвращающий экземпляр текущего типа).
  • Разрешите аргументы конструктора и активируйте экземпляр.
  • Заполните доступные для записи общедоступные свойства и поля сгенерированными значениями.

При текущем подходе, как вы ранее заметили, вы не можете каким-либо образом контролировать ограничения зависимостей.

У нас есть несколько точек настройки, чтобы указать, как создавать определенные типы, но они относительно просты и не поддерживают эти сложные правила.

Как лучше всего создать экземпляр такой структуры в AutoFixture? Я имею в виду, что я, очевидно, напишу настройку, но что бы вы туда добавили?

  • Вы бы сгенерировали случайные числа и цикл до тех пор, пока одно из них не станет простым (или, конечно, выполните алгоритм генерации простых чисел)? Это могло быть приемлемо для такого рода ограничений, но если бы ограничение было более сложным для соблюдения, это быстро стало бы дорогостоящим.

  • Не могли бы вы предоставить конечный список некоторых приемлемых значений?

Что ж, к сожалению, я не вижу здесь серебряной пули и подход зависит от ситуации. Если вы не полагаетесь на то, что значение будет слишком случайным или одиночная SUT потребляет только 1-2 простых числа, тогда может быть нормально жестко закодировать простые числа и выбрать из них (у нас есть встроенный помощник ElementsBulider<> для тех случаев). С другой стороны, если вам нужен большой список простых чисел и вы работаете с длинными последовательностями простых чисел, то, вероятно, лучше закодировать алгоритм для их динамического генерирования.

Более того, предположим теперь, что я пытаюсь создать экземпляр чего-то, что принимает несколько аргументов, которые теоретически могут быть случайными по отдельности, но при этом будет выполняться некоторая проверка между ними (например, argA может находиться в этом диапазоне значений, только если argB истинно, и argC должен соответствовать различным правилам проверки в зависимости от значения argA, или свойство argC.X должно совпадать со свойством argA.X, что-то в этом роде).

Что бы вы сделали в этом случае?

Действительно хороший вопрос и, к сожалению, AutoFixture не позволяет решить его прямо из коробки. Обычно я пытаюсь изолировать настройки для каждого типа, поэтому настройка для одного типа создает элементы управления только для одного типа. Но в моих случаях типы независимы, и, очевидно, в вашем случае это не сработает. Кроме того, AutoFixture не предоставляет контекст прямо из коробки, поэтому, когда вы пишете настройку для определенного типа, вы не можете четко понять контекст, в котором вы создаете объект (внутренне называемый образцом).

Подумав, я бы сказал, что обычно рекомендую следующую стратегию:

  • Попробуйте создать настройку для каждого типа таким образом, чтобы он управлял созданием только одного типа объекта.
  • Если вам нужно создать зависимости с определенными ограничениями, лучше также активировать эти зависимости в настройке. Если ваша зависимость изменяема, вы можете попросить AutoFixture создать для вас зависимость, а затем настроить ее таким образом, чтобы она стала совместимой.

Таким образом, вы не будете слишком сильно противоречить внутренней архитектуре, и будет понятно, как она работает. Конечно, потенциально этот способ очень многословен.

Если случаи со сложными ограничениями не так распространены, существующих возможностей может быть достаточно для вас. Но если ваша модель предметной области действительно полна таких случаев, откровенно говоря, AutoFixture может быть не лучшим инструментом для вас. Наверное, на рынке есть инструменты получше, которые позволяют наиболее элегантно решать такие проблемы. Конечно, стоит упомянуть, что AutoFixture очень гибкая, и вы можете переопределить почти все, поэтому вы всегда можете создать свой собственный DSL поверх ядра AutoFixture ... Но вы должны оценить, какой способ дешевле для вас 😉

Давайте также спросим у @ploeh его мысли. Обычно ответы Марка глубокие, и он сначала пытается найти первопричину, а не устранять последствия 😅

Если есть еще вопросы - задавайте! Я всегда буду рад на них ответить.

PS FWIW, я решил предоставить вам образец, в котором я попытался поиграть с AutoFixture и решить аналогичную проблему (я попытался сделать это простым, и в вашем случае он может не работать полностью):


Нажмите, чтобы увидеть исходный код

`` С #
используя Систему;
с помощью AutoFixture;
с использованием AutoFixture.Xunit2;
с помощью Xunit;

пространство имен AutoFixturePlayground
{
общедоступный статический класс Util
{
public static bool IsPrime (целое число)
{
// Скопировано с 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, поэтому время очень ограниченный ресурс: pensive: Вопрос сложный, поэтому хочу подумать обо всех возможных способах, прежде чем публиковать ответ.

Спасибо за терпение, следите за новостями: wink:

Привет,
Есть новости по этому поводу?
Никакого давления (я знаю, что такое упражнение 😄, плюс оно не совсем блокирует, мне просто нужен квалифицированный совет), просто чтобы знать, есть ли у вас некоторая видимость.
Большое спасибо!

Добрый день! Наконец, я выделил немного текста для ответа - извините за запоздалый ответ 😊

Прежде всего обратите внимание, что ядро ​​AutoFixture довольно простое, и у нас нет встроенной поддержки сложных деревьев с ограничениями. Вкратце, стратегия создания выглядит следующим образом:

  • Найдите общедоступный конструктор или статический фабричный метод (статический метод, возвращающий экземпляр текущего типа).
  • Разрешите аргументы конструктора и активируйте экземпляр.
  • Заполните доступные для записи общедоступные свойства и поля сгенерированными значениями.

При текущем подходе, как вы ранее заметили, вы не можете каким-либо образом контролировать ограничения зависимостей.

У нас есть несколько точек настройки, чтобы указать, как создавать определенные типы, но они относительно просты и не поддерживают эти сложные правила.

Как лучше всего создать экземпляр такой структуры в AutoFixture? Я имею в виду, что я, очевидно, напишу настройку, но что бы вы туда добавили?

  • Вы бы сгенерировали случайные числа и цикл до тех пор, пока одно из них не станет простым (или, конечно, выполните алгоритм генерации простых чисел)? Это могло быть приемлемо для такого рода ограничений, но если бы ограничение было более сложным для соблюдения, это быстро стало бы дорогостоящим.

  • Не могли бы вы предоставить конечный список некоторых приемлемых значений?

Что ж, к сожалению, я не вижу здесь серебряной пули и подход зависит от ситуации. Если вы не полагаетесь на то, что значение будет слишком случайным или одиночная SUT потребляет только 1-2 простых числа, тогда может быть нормально жестко закодировать простые числа и выбрать из них (у нас есть встроенный помощник ElementsBulider<> для тех случаев). С другой стороны, если вам нужен большой список простых чисел и вы работаете с длинными последовательностями простых чисел, то, вероятно, лучше закодировать алгоритм для их динамического генерирования.

Более того, предположим теперь, что я пытаюсь создать экземпляр чего-то, что принимает несколько аргументов, которые теоретически могут быть случайными по отдельности, но при этом будет выполняться некоторая проверка между ними (например, argA может находиться в этом диапазоне значений, только если argB истинно, и argC должен соответствовать различным правилам проверки в зависимости от значения argA, или свойство argC.X должно совпадать со свойством argA.X, что-то в этом роде).

Что бы вы сделали в этом случае?

Действительно хороший вопрос и, к сожалению, AutoFixture не позволяет решить его прямо из коробки. Обычно я пытаюсь изолировать настройки для каждого типа, поэтому настройка для одного типа создает элементы управления только для одного типа. Но в моих случаях типы независимы, и, очевидно, в вашем случае это не сработает. Кроме того, AutoFixture не предоставляет контекст прямо из коробки, поэтому, когда вы пишете настройку для определенного типа, вы не можете четко понять контекст, в котором вы создаете объект (внутренне называемый образцом).

Подумав, я бы сказал, что обычно рекомендую следующую стратегию:

  • Попробуйте создать настройку для каждого типа таким образом, чтобы он управлял созданием только одного типа объекта.
  • Если вам нужно создать зависимости с определенными ограничениями, лучше также активировать эти зависимости в настройке. Если ваша зависимость изменяема, вы можете попросить AutoFixture создать для вас зависимость, а затем настроить ее таким образом, чтобы она стала совместимой.

Таким образом, вы не будете слишком сильно противоречить внутренней архитектуре, и будет понятно, как она работает. Конечно, потенциально этот способ очень многословен.

Если случаи со сложными ограничениями не так распространены, существующих возможностей может быть достаточно для вас. Но если ваша модель предметной области действительно полна таких случаев, откровенно говоря, AutoFixture может быть не лучшим инструментом для вас. Наверное, на рынке есть инструменты получше, которые позволяют наиболее элегантно решать такие проблемы. Конечно, стоит упомянуть, что AutoFixture очень гибкая, и вы можете переопределить почти все, поэтому вы всегда можете создать свой собственный DSL поверх ядра AutoFixture ... Но вы должны оценить, какой способ дешевле для вас 😉

Давайте также спросим у @ploeh его мысли. Обычно ответы Марка глубокие, и он сначала пытается найти первопричину, а не устранять последствия 😅

Если есть еще вопросы - задавайте! Я всегда буду рад на них ответить.

PS FWIW, я решил предоставить вам образец, в котором я попытался поиграть с AutoFixture и решить аналогичную проблему (я попытался сделать это простым, и в вашем случае он может не работать полностью):


Нажмите, чтобы увидеть исходный код

`` С #
используя Систему;
с помощью AutoFixture;
с использованием AutoFixture.Xunit2;
с помощью Xunit;

пространство имен AutoFixturePlayground
{
общедоступный статический класс Util
{
public static bool IsPrime (целое число)
{
// Скопировано с 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, так и с тестированием на основе свойств показывает, что есть два основных способа решения подобных проблем:

  • Фильтрация
  • Алгоритмическое создание

(Пока я пишу, моя интуиция подсказывает, что это могут быть _катаморфизмы_ и _анаморфизмы_ соответственно, но мне придется подумать об этом еще немного, так что это в сторону, в основном, примечание для меня.)

Если _most_ случайно сгенерированные значения будут соответствовать любым ограничениям, в которые вы должны входить, то использование существующего генератора, но отбрасывание случайного неподходящего значения может быть самым простым способом решения проблемы.

Если, с другой стороны, фильтр будет означать отбрасывание большинства случайных данных, вам вместо этого придется придумать алгоритм, который, возможно, основанный на случайных начальных значениях, будет генерировать значения, которые соответствуют рассматриваемым ограничениям.

Несколько лет назад я выступил с докладом, показав несколько простых примеров обоих подходов в контексте FsCheck . Эта презентация на самом деле является развитием выступления, в котором использовался тот же подход, но только с использованием AutoFixture. К сожалению, записи этого разговора не существует.

Требование простого числа можно было удовлетворить обоими способами.

Подход с фильтром состоит в том, чтобы генерировать неограниченные числа, а затем отбрасывать числа, пока вы не получите одно, которое действительно является простым.

Алгоритмический подход заключался бы в использовании такого алгоритма, как простое решето, для генерации простого числа. Однако это не случайно, поэтому можно подумать, как его рандомизировать.

Общий вопрос о том, как работать с ограниченными значениями в AutoFixture, возник почти сразу же, как только другие люди начали изучать библиотеку, и тогда я написал статью, на которую до сих пор ссылаюсь: http://blog.ploeh.dk/2009/ 05/01 / DealingWithConstrainedInput

Что касается вопроса о множестве взаимосвязанных ценностей, я не хочу давать каких-либо общих указаний. Такие вопросы часто являются проблемами XY. Во многих случаях, когда я понимаю особенности, альтернативный дизайн может решить проблемы не только с AutoFixture, но и с самой производственной базой кода.

Однако даже при наличии проблем XY все равно будут ситуации, когда это может быть законным беспокойством, но я бы предпочел разбираться с ними в индивидуальном порядке, поскольку, по моему опыту, они редкий.

Итак, если у вас есть конкретный пример этого, я могу помочь, но не думаю, что смогу осмысленно ответить на общий вопрос.

@ploeh Большое спасибо за этот ответ, который подтверждает подходы, которые я рассматривал (и вы заинтересовали меня ката- и анаморфизмами 😃).
Я полностью согласен с тем, что взаимозависимые значения в основном являются проблемой XY (по крайней мере, в моем случае), дело в том, что при работе с устаревшим (непроверенным 😢) кодом работа с этими значениями в любом случае была хорошим началом для написания некоторых тестов, пока мы не получим время правильно рефакторировать это.

В любом случае, оба ваших ответа довольно хорошо решают проблему, я думаю, я готов продолжить работу.
Спасибо!

Кстати, я забыл упомянуть, что я имел в виду свой ответ только как дополнение к @zvirja . Там уже хороший ответ 👍

Я по-другому не пошел

Была ли эта страница полезной?
0 / 5 - 0 рейтинги