1

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

JansthcirlU
  • 688
  • 5
  • 21
  • 1
    "sniff out any possible edge cases by bombarding both methods with a ton of random data" that's not how edge cases work. Edge cases work by thinking about the logic of your code and applying a small, targeted set of test cases. For example, if you were testing a mathematical "absolute value" function, you wouldn't test all numbers between -1 and -1 million, that's wasteful. Just test -1, maybe -10, 0, 1, and 10. – gunr2171 Feb 09 '21 at 16:15
  • Right, but I don't really know what can go wrong exactly, so I don't think it will be enough for me to test only 10 or 100 possible dates of birth to discover errors. By sheer luck, I might get 100 well-behaved dates of birth (no leap years, always born before before the randomly generated time of day, etc.) but I will likely not get a million well-behaved dates. – JansthcirlU Feb 09 '21 at 16:19
  • The problem with using `Random` in a test case is that one time you run it, it might fail, and the next time you run it, _without changing any code_, it could pass. Unit tests need to be 100% repeatable. That's what Theories are for. You craft specially designed edge-case tests (which become method parameters) so you can validate your method, rather than blasting it with data and praying it works. – gunr2171 Feb 09 '21 at 16:22
  • @gunr2171 I understand, but the testing of both methods depends on the correctness of both, so in essence I can never be sure if either method really works until I brute force possible errors that can be caused by either one of the methods. – JansthcirlU Feb 09 '21 at 16:24
  • To add to gunr's comments: You need to look into `GetAge` to figure out possible edgecases. UnitTests don't have to be Black Box Tests. – Fildor Feb 09 '21 at 16:25
  • Would be easier to tell you something useful, if you added both implementations. – Fildor Feb 09 '21 at 16:26
  • 1
    For general usage of InlineData, MemberData, ClassData maybe see https://andrewlock.net/creating-parameterised-tests-in-xunit-with-inlinedata-classdata-and-memberdata/ – Fildor Feb 09 '21 at 16:32
  • 1
    I've added my current implementations to the question @Fildor. – JansthcirlU Feb 09 '21 at 16:34
  • I don't necessarily need the same date of birth, but I do want to test if `GetAge(GenerateDateOfBirth(someAge)) == someAge`. You're right that I didn't take time zones and such into account. – JansthcirlU Feb 09 '21 at 16:39

0 Answers0