As outlined elsewhere, I'd recommend a solution where you let test-driven development provide feedback on your design. Instead of treating humidity and temperature as primitives, refactor to a good domain model. As an example, create a new value object for both:
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();
}
}
The type Celcius
looks similar:
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();
}
}
This guarantees that if you have a Humidity
or Celcius
object, they're valid, because they protect their invariants. This is valuable in your production code, but also provides testing benefits.
Weather
simply looks like this, now:
public class Weather
{
public Humidity Humidity { get; }
public Celcius Temperature { get; }
public Weather(Humidity humidity, Celcius temperature)
{
this.Humidity = humidity;
this.Temperature = temperature;
}
}
You can override Equals
and GetHashCode
for Weather
as well, if you like, but it's not important for this example.
For AutoFixture, you can now define customizations for both types:
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);
}
}
}
and
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);
}
}
}
You can collect those (and others) in a CompositeCustomization
:
public class MyConventions : CompositeCustomization
{
public MyConventions() : base(
new CelciusCustomization(),
new HumidityCustomization())
{
}
}
Now you can write tests as simple as this:
[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);
}
This test passes.
Granted, this looks like a lot of work for a single test, but the point is that if you collect all domain-specific customizations in MyConventions
, you can reuse that single convention across hundreds of tests, because it guarantess that all domain objects are valid.
Not only does it make your test code more maintainable, it also makes you production code more maintainable.