使用约定创建对象

时间:2017-01-05 15:10:35

标签: unit-testing xunit autofixture

我想单独测试一个天气解析方法。我的第一种方法是让autofixture创建一个天气对象,然后从中创建查询响应。但天气等级包含多个限制:

  • 湿度是一个百分比值,必须介于1-100
  • 之间
  • 温度必须高于最小值,具体取决于温度单位

是否有可能解决这个问题,是否值得使用这种方法或只是硬编码查询响应和预期的天气对象?

1 个答案:

答案 0 :(得分:1)

作为outlined elsewhere,我建议您使用测试驱动开发提供有关您设计的反馈的解决方案。而不是将湿度和温度视为基元,refactor to a good domain model。例如,为两者创建一个新的值对象:

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();
    }
}

这保证了如果你有一个HumidityCelcius对象,它们就是有效的,因为它们可以保护它们的不变量。这在您的生产代码中很有价值,但也提供了测试优势。

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的{​​{1}}和GetHashCode,但这对此示例并不重要。

对于AutoFixture,您现在可以为这两种类型定义自定义:

Weather

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); } 中收集所有特定于域的自定义项,则可以在数百个测试中重用该单个约定,因为它保证所有域对象都有效。

它不仅可以使您的测试代码更易于维护,还可以使您的生产代码更易于维护。