mardi 9 février 2021

Unit testing theories on collections xUnit

Original question

Let's say I have a class method GetAge(DateTime dateOfBirth) and I want to test if the age that comes out is correct. To do this, I create a helper method called GenerateDateOfBirth(int age) that returns a date of birth that should yield an age of age.

However, I don't know if my GenerateDateOfBirth method works, so I would like to bulk test a collection of ages to feed into GenerateDateOfBirth, where each generated date of birth should yield the original age when fed back into GetAge, something along the lines of:

[Fact]
public void CrossTestGetAgeAndGenerateDateOfBirth()
{
    var ages = new List<int>(); // A reasonably large list of possible ages
    var rng = new Random();
    for (int i = 0; i < 1_000_000; i++)
    {
        ages.Add(rng.Next(0, 1000)); // Arbitrary upper bound on a person's age
    }
    foreach (var realAge in ages)
    {
        var dateOfBirth = GenerateDateOfBirth(realAge);
        var calculatedAge = GetAge(dateOfBirth);
        Assert.Equal(realAge, calculatedAge); // If false, at least one of the methods is bugged
    }
}

I have seen that xUnit allows testing on data, by using attributes such as InlineData and MemberData, so I was wondering if I could generate a list of integers an then do a data-driven test on all of the "age" values like so:

public List<int> Ages { get; set; } = GetAges(0, 1000, 1_000_000); // Same arbitrary bounds as above
public List<int> GetAges(int minAge, int maxAge, int count)
{
    var ages = new List<int>();
    var rng = new Random();
    for (int i = 0; i < count; i++)
    {
        ages.Add(rng.Next(minAge, maxAge));
    }
    return ages;
}

[Theory]
[/* something with Ages */]
public void AgeFromDateOfBirthFromAge(/* what goes here? */)
{
    var randomDateOfBirth = GenerateDateOfBirth(age); // Where age comes from Ages
    var calculatedAge = GetAge(randomDateOfBirth);
    Assert.Equal(age, calculatedAge);
}

This seems more concise and readable and it does the same thing still. My goal (before I can write actual unit tests), is to sniff out any possible edge cases by bombarding both methods with a ton of random data. Once one of the methods seems to work well enough, ideally I can use it to fix the other and then do a final review of the (hopefully) few edge cases that might remain. Is there a way to do this?

Current implementations of both methods

Method to get age

public int GetAge(DateTime dob)
{
    var now = DateTime.Now;

    // Time difference up to the day
    int deltaDays = now.Day - dob.Day;
    int deltaMonths = now.Month - dob.Month;
    int deltaYears = now.Year - dob.Year;
    int age = (deltaDays + 100 * deltaMonths + 10000 * deltaYears) / 10000;

    // Check if time of day has also been reached
    var timeNotReached = now.TimeOfDay.Ticks < dob.TimeOfDay.Ticks;

    return (deltaDays == 0 && deltaMonths == 0 && timeNotReached) ? age - 1 : age;
}

Method to generate a date of birth

public DateTime GenerateDateOfBirth(int age = 18)
{
    var rng = new Random();
    var now = DateTime.Now;

    // Get allowed values for birth date
    var maxMonth = now.Month + 1;
    var maxDay = now.Day + 1;
    var maxHour = now.Hour + 1;
    var maxMinute = now.Minute + 1;
    var maxSecond = now.Second + 1;
    var maxMillisecond = now.Millisecond;

    // Choose random allowed values
    var birthYear = now.Year - age;
    var birthMonth = rng.Next(1, maxMonth);
    var birthDay = birthMonth != now.Month ? rng.Next(1, DateTime.DaysInMonth(birthYear, birthMonth) + 1) : rng.Next(1, maxDay);
    var birthHour = rng.Next(1, maxHour);
    var birthMinute = rng.Next(1, maxMinute);
    var birthSecond = rng.Next(1, maxSecond);
    var birthMillisecond = rng.Next(maxMillisecond);

    var dateOfBirth = new DateTime(
        birthYear,
        birthMonth,
        birthDay,
        birthHour,
        birthMinute,
        birthSecond,
        birthMillisecond
    );
    return dateOfBirth;
}

Aucun commentaire:

Enregistrer un commentaire