Создание объектов с соглашениями

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

  • Влажность указывается в процентах и ​​должна находиться в диапазоне от 1 до 100.
  • Температура должна быть выше минимума в зависимости от единицы измерения температуры.

Можно ли решить эту проблему и стоит ли использовать этот подход или просто жестко кодировать ответ на запрос и ожидаемый объект погоды?


person Community    schedule 05.01.2017    source источник
comment
Если ваш модульный тест тестирует потребителя объекта погоды, я бы просто жестко запрограммировал его. Но, как и во многих других случаях, это зависит...   -  person mxmissile    schedule 05.01.2017
comment
Вот ответ на аналогичный вопрос: stackoverflow.com/a/22333452/126014   -  person Mark Seemann    schedule 05.01.2017
comment
@mxmissile Да, парсер является потребителем объекта погоды.   -  person    schedule 05.01.2017
comment
Я бы просто заглушил результаты. Проверьте вычисление влажности в отдельном модульном тесте.   -  person mxmissile    schedule 05.01.2017


Ответы (1)


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

public struct Humidity
{
    public readonly byte percentage;

    public Humidity(byte percentage)
    {
        if (100 < percentage)
            throw new ArgumentOutOfRangeException(
                nameof(percentage),
                "Percentage must be a number between 0 and 100.");

        this.percentage = percentage;
    }

    public static explicit operator byte(Humidity h)
    {
        return h.percentage;
    }

    public static explicit operator int(Humidity h)
    {
        return h.percentage;
    }

    public override bool Equals(object obj)
    {
        if (obj is Humidity)
            return ((Humidity)obj).percentage == this.percentage;

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return this.percentage.GetHashCode();
    }
}

Тип Celcius выглядит аналогично:

public struct Celcius
{
    private readonly decimal degrees;

    public Celcius(decimal degrees)
    {
        if (degrees < -273.15m)
            throw new ArgumentOutOfRangeException(
                nameof(degrees),
                "Degrees Celsius must be equal to, or above, absolute zero.");

        this.degrees = degrees;
    }

    public static explicit operator decimal(Celcius c)
    {
        return c.degrees;
    }

    public override bool Equals(object obj)
    {
        if (obj is Celcius)
            return ((Celcius)obj).degrees == this.degrees;

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return this.degrees.GetHashCode();
    }
}

Это гарантирует, что если у вас есть объект Humidity или Celcius, они действительны, потому что они защищают свои инварианты. Это ценно в вашем производственном коде, но также дает преимущества при тестировании.

Weather теперь просто выглядит так:

public class Weather
{
    public Humidity Humidity { get; }
    public Celcius Temperature { get; }

    public Weather(Humidity humidity, Celcius temperature)
    {
        this.Humidity = humidity;
        this.Temperature = temperature;
    }
}

Вы также можете переопределить Equals и GetHashCode для Weather, если хотите, но в данном примере это не важно.

Для AutoFixture теперь вы можете определить настройки для обоих типов:

public class HumidityCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(new HumidityBuilder());
    }

    private class HumidityBuilder : ISpecimenBuilder
    {
        public object Create(object request, ISpecimenContext context)
        {
            var t = request as Type;
            if (t == null || t != typeof(Humidity))
                return new NoSpecimen();

            var d =
                context.Resolve(
                    new RangedNumberRequest(
                        typeof(byte),
                        byte.MinValue,
                        (byte)100));
            return new Humidity((byte)d);
        }
    }
}

а также

public class CelciusCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(new CelciusBuilder());
    }

    private class CelciusBuilder : ISpecimenBuilder
    {
        public object Create(object request, ISpecimenContext context)
        {
            var t = request as Type;
            if (t == null || t != typeof(Celcius))
                return new NoSpecimen();

            var d = 
                context.Resolve(
                    new RangedNumberRequest(
                        typeof(decimal),
                        -273.15m,
                        decimal.MaxValue));
            return new Celcius((decimal)d);
        }
    }
}

Вы можете собрать те (и другие) в CompositeCustomization:

public class MyConventions : CompositeCustomization
{
    public MyConventions() : base(
        new CelciusCustomization(),
        new HumidityCustomization())
    {
    }
}

Теперь вы можете писать такие простые тесты:

[Fact]
public void FixtureCanCreateValidWeather()
{
    var fixture = new Fixture().Customize(new MyConventions());

    var actual = fixture.Create<Weather>();

    Assert.True((int)actual.Humidity <= 100);
    Assert.True(-273.15m <= (decimal)actual.Temperature);
}

Этот тест проходит.

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

Это не только делает ваш тестовый код более удобным в сопровождении, но и рабочий код.

person Mark Seemann    schedule 13.10.2017
comment
Это отличный ответ, но, похоже, есть немало работы по созданию действительных примитивов домена (при условии, что у вас даже есть или вы можете провести рефакторинг для таких типов, поддерживающих инвариант). Декларативные подходы, которые может понять AutoFixture, были бы идеальными, но аннотирование с помощью атрибутов также кажется неправильным. Интересно, есть ли третий подход, или мы выходим за пределы выразительности С#? - person Jack Ukleja; 02.11.2017
comment
@Schneider Если есть третий подход, я о нем не знаю. Я искал много лет и, наконец, отказался от C# в пользу F#. В F# вы делаете определение таких типов декларативным и лаконичным способом. - person Mark Seemann; 03.11.2017