Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Static Data from xunit MemberData function is computed twice

I'm having some trouble with computed data from a static class in a C# Xunit test being computed twice.

The actual production code this would be used for is much more complicated, but the code that follows is enough to exhibit the issue I am seeing.

In the code below I have a randomly generated, lazily loaded int seeded off of the current time.

All I am testing here is that this property is equal to itself. I insert the property's value into the test via a MemberData function.

Since the property ought to only be initialized once, I'd expect that this test should always pass. I would expect that the static field would be initialized when the RandomIntMemberData function is run and never again.

However, this test consistently fails. The value inserted into the test, and the value tested against are always different.

Further if I debug, I only see the initialization code being hit once. That is, the value being tested. I never see the initialization of the value being tested against.

Am I misunderstanding something, or is Xunit doing some weird behind the scenes magic to setup it's input data, then initializing the value again when the test is actually run?

Minimal Code to Reproduce Bug

public static class TestRandoIntStaticClass
{
    private static readonly Lazy<int> LazyRandomInt = new Lazy<int>(() =>
    {
        // lazily initialize a random interger seeded off of the current time
        // according to readings, this should happen only once
        return new Random((int) DateTime.Now.Ticks).Next();
    });

    // according to readings, this should be a thread safe operation
    public static int RandomInt => LazyRandomInt.Value; 
}

The Test

public class TestClass
{
    public static IEnumerable<object[]> RandomIntMemberData()
    {
        var randomInt = new List<object[]>
        {
            new object[] {TestRandoIntStaticClass.RandomInt},
        };

        return randomInt as IEnumerable<object[]>;
    }

    [Theory]
    [MemberData(nameof(RandomIntMemberData))]
    public void RandoTest(int rando)
    {
        // these two ought to be equal if TestRandoIntStaticClass.RandomInt is only initialized once 
        Assert.True(rando == TestRandoIntStaticClass.RandomInt,
                    $"{nameof(rando)} = {rando} but {nameof(TestRandoIntStaticClass.RandomInt)} = {TestRandoIntStaticClass.RandomInt}");
    }
}
like image 544
wilbur Avatar asked Oct 30 '18 19:10

wilbur


People also ask

What is difference between Fact and Theory in xUnit?

Fact vs Theory Tests The primary difference between fact and theory tests in xUnit is whether the test has any parameters. Theory tests take multiple different inputs and hold true for a particular set of data, whereas a Fact is always true, and tests invariant conditions.

What is Fact attribute in xUnit?

xUnit uses the [Fact] attribute to denote a parameterless unit test, which tests invariants in your code. In contrast, the [Theory] attribute denotes a parameterised test that is true for a subset of data. That data can be supplied in a number of ways, but the most common is with an [InlineData] attribute.

What is inline data in xUnit?

[InlineData] allows us to specify that a separate test is run on a particular Theory method for each instance of [InlineData] .


1 Answers

At the time of tests discovery, Visual Studio Xunit console runner creates AppDomain with test data for all attributes like MemberData, ClassData, DataAttribute so all data are just saved in memory after build (that is also why XUnit require classes to be serializable).

We can verify this by adding a simple logger to your methods:

namespace XUnitTestProject1
{
    public class TestClass
    {
        public static IEnumerable<object[]> RandomIntMemberData()
        {
            var randomInt = new List<object[]>
            {
                new object[]
                    {TestRandoIntStaticClass.RandomInt},
            };
            return randomInt;
        }

        [Theory]
        [MemberData(nameof(RandomIntMemberData))]
        public void RandoTest(int rando)
        {
            // these two ought to be equal if TestRandoIntStaticClass.RandomInt is only initialized once 
            Assert.True(rando == TestRandoIntStaticClass.RandomInt, $"{nameof(rando)} = {rando} but {nameof(TestRandoIntStaticClass.RandomInt)} = {TestRandoIntStaticClass.RandomInt}");
        }

    }

    public static class TestRandoIntStaticClass
    {
        private static readonly Lazy<int> LazyRandomInt = new Lazy<int>(() =>
        {   // lazily initialize a random interger seeded off of the current time
            // according to readings, this should happen only once
            var randomValue = new Random((int) DateTime.Now.Ticks).Next();

            File.AppendAllText(@"D:\var\log.txt", $"Call TestRandoIntStaticClass {randomValue}; ThreadId {Thread.CurrentThread.ManagedThreadId} " + Environment.NewLine);
            return randomValue;
        });
       
        public static int RandomInt => LazyRandomInt.Value; // according to readings, this should be a thread safe operation
    }
}

As a result we see in logs:

> Call TestRandoIntStaticClass 1846311153; ThreadId 11  
> Call TestRandoIntStaticClass 1007825738; ThreadId 14

And in test execution result

rando = 1846311153 but RandomInt = 1007825738
Expected: True
Actual:   False
   at 

However, if you use dotnet test will be successful because 'data generation' and test run will be launched on one process

like image 118
svoychik Avatar answered Oct 10 '22 06:10

svoychik