зацикливание в модульном тесте плохо?

У меня есть модульный тест, основанный на случайном броске кубиков. Я бросаю 20-гранный кубик, и если значение равно 20, это считается критическим попаданием.

Что я делаю прямо сейчас, так это бросаю 20-гранный кубик до 300 раз. Если хотя бы один из этих бросков выпал на 20, я знаю, что получил критическое попадание.

Вот как выглядит код:

public class DiceRoll
{
    public int Value { get; set; }
    public bool IsCritical { get; set; }

    // code here that sets IsCritical to true if "Value" gets set to 20
}

[Test]
public void DiceCanRollCriticalStrikes()
{
    bool IsSuccessful = false;
    DiceRoll diceRoll = new DiceRoll();

    for(int i=0; i<300; i++)
    {
        diceRoll.Value = Dice.Roll(1, 20); // roll 20 sided die once
        if(diceRoll.Value == 20 && diceRoll.IsCritical)
        {
            IsSuccessful = true;
            break;
        }
    }

    if(IsSuccessful)
        // test passed
    else
        // test failed 
}

Хотя тест делает именно то, что я хочу, я не могу не чувствовать, что делаю что-то не так.

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


person mikedev    schedule 24.01.2010    source источник


Ответы (3)


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

Я бы рассмотрел возможность извлечения логики броска костей из класса Dice через интерфейс (например, "IDiceRoller"). Затем вы можете реализовать случайный бросок кубиков в своем приложении и еще один бросок кубиков в своем проекте модульного тестирования. Это будет всегда возвращать предопределенное значение. Таким образом, вы можете писать тесты для определенных значений кубиков, не прибегая к зацикливанию и надежде, что значение появится.

Пример:

(код в вашем приложении)

public interface IDiceRoller
{
    int GetValue(int lowerBound, int upperBound);
}

public class DefaultRoller : IDiceRoller
{
    public int GetValue(int lowerBound, int upperBound)
    {
        // return random value between lowerBound and upperBound
    }
}

public class Dice
{
    private static IDiceRoller _diceRoller = new DefaultRoller();

    public static void SetDiceRoller(IDiceRoller diceRoller)
    {
        _diceRoller = diceRoller;
    }

    public static void Roll(int lowerBound, int upperBound)
    {
        int newValue = _diceRoller.GetValue(lowerBound, upperBound);
        // use newValue
    }
}

... и в вашем проекте модульного тестирования:

internal class MockedDiceRoller : IDiceRoller
{
    public int Value { get; set; }

    public int GetValue(int lowerBound, int upperBound)
    {
        return this.Value;
    }
}

Теперь в своем модульном тесте вы можете создать MockedDiceRoller, установить значение, которое вы хотите, чтобы кости получили, установить имитацию ролика кости в классе Dice, бросить и проверить это поведение:

MockedDiceRoller diceRoller = new MockedDiceRoller();
diceRoller.Value = 20;
Dice.SetDiceRoller(diceRoller);

Dice.Roll(1, 20);
Assert.IsTrue(Dice.IsCritical);
person Fredrik Mörk    schedule 24.01.2010
comment
Напоминает комикс xkcd: int rolldice() { return 4; } // генерируется реальным броском кубика; гарантированно будет случайным. - person John Feminella; 24.01.2010
comment
Хорошо, это именно то, что я искал. Отличное объяснение, и я узнаю больше о модульном тестировании. Огромное спасибо. - person mikedev; 24.01.2010
comment
Также обратите внимание на насмешки. Это позволяет вам установить некоторые хорошо определенные сценарии; вам не нужно создавать класс DefaultRoller, так как он будет автоматически создан фиктивной структурой. - person lmsasu; 24.01.2010

Хотя тест делает именно то, что я хочу, я не могу не чувствовать, что делаю что-то не так.

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

Вместо этого выполните модульный тест, который проверяет, зарегистрирован ли критический удар IFF (если-и-только-если) выпало 20.

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

person micahtan    schedule 24.01.2010
comment
Но я все еще делаю это в цикле? Или мне изменить дизайн моего класса Dice, чтобы я мог форсировать свои собственные результаты? Например, даже проверка IFF на выпадение 20 означает, что мне нужно выполнить цикл, скажем, 300 раз, чтобы убедиться, что выпало случайное 20. - person mikedev; 24.01.2010
comment
Нет, два разных класса модульных тестов. Один модульный тест для класса DiceRoll и отношения между свойствами Value и IsCritical. Еще один модульный тест для метода Roll класса Dice, который вызывает цикл несколько раз (10 КБ? может больше?) и проверяет, что он дает вам приемлемый уровень случайности. Если вы просто создаете оболочку для класса .NET Random, вы можете не утруждать себя написанием теста Dice.Roll. - person micahtan; 24.01.2010

А теперь противоположное мнение:

Шанс не получить 20 из 300 бросков составляет примерно 1 к 5 миллионам. Когда-нибудь ваш модульный тест может не пройти из-за этого, но он пройдет в следующий раз, когда вы его протестируете, даже если вы не будете трогать код.

Я хочу сказать, что ваш тест, вероятно, никогда не провалится из-за полосы неудач, а если и случится, то что с того? Усилия, которые вы потратите на усиление этого тестового примера, вероятно, лучше потратить на какую-то другую часть вашего проекта. Если вы хотите быть более параноидальным, не усложняя тест, измените его на 400 бросков (вероятность неудачи: 1 на 814 миллионов).

person mob    schedule 24.01.2010
comment
Я согласен с понятием 1 из 5 миллионов. Моя проблема с исходным тестом в том, что он тестирует две разные вещи: 1) 20 = критический удар. 2) Что кости в конечном итоге выпадут 20. Вам не нужно повторять 300 раз, чтобы выполнить тест № 1, и в зависимости от того, как реализован № 2, вы, вероятно, можете даже не писать этот тест. - person micahtan; 24.01.2010
comment
Хотя я согласен с тем, что вы говорите, причина, по которой я задаю этот вопрос, заключается в том, чтобы узнать больше о правильном модульном тестировании. - person mikedev; 24.01.2010