Autofixture: проблема при создании типов значений

Отладив с помощью xUnit.net методы тестирования Test1 и Test2 следующего кода и поставив точку останова и конец CreateValueAndReferenceType(), вы увидите, что переменная valueType одинакова в обоих прогонах, тогда как переменная referenceType изменена. Первое для меня удивительно и также является проблемой (я добавил строку со строковым типом только для полноты).

public class MyFixture : Fixture
{
    public void CreateValueAndReferenceType()
    {
        var valueType = this.Create<int>();
        var referenceTye = this.Create<string>();
    }
}

public class TestClass1
{
    [Fact]
    public void Test1()
    {
        var myFixture = new MyFixture();
        myFixture.CreateValueAndReferenceType();
    }
}

public class TestClass2
{
    [Fact]
    public void Test2()
    {
        var myFixture = new MyFixture();
        myFixture.CreateValueAndReferenceType();
    }
}

person Felix Liebrecht    schedule 20.06.2016    source источник


Ответы (1)


Я думаю, что вы видите основную проблему, связанную с генерацией псевдослучайных чисел в .NET (IIRC, другие платформы имеют аналогичные проблемы). По сути, System.Random является детерминированным, но инициализируется случайным начальным числом, которое, среди прочего, зависит от текущего времени компьютера. Если вы создаете экземпляры Random в тесном цикле, код выполняется быстрее, чем точность системных часов. Что-то вроде этого:

for (int i = 0; i < 10; i++)
    Console.Write(new Random().Next(0, 9));

часто будет выводить такой вывод:

5555555555

Большинство значений в AutoFixture генерируются различными экземплярами Random — исключение составляет тип string, значения которого генерируются Guid.NewGuid().ToString().

Я думаю, что вы видите это из-за параллельного выполнения xUnit.net.

Чтобы точно определить проблему, я перефразировал проблему так, чтобы она не зависела от отладки или наследования:

public static class Reporter
{
    public static void CreateValueAndReferenceType(
        IFixture fixture,
        ITestOutputHelper @out)
    {
        var valueType = fixture.Create<int>();
        var referenceTye = fixture.Create<string>();

        @out.WriteLine("valueType: {0}", valueType);
        @out.WriteLine("referenceType: {0}", referenceTye);
    }
}

public class TestClass1
{
    private readonly ITestOutputHelper @out;

    public TestClass1(ITestOutputHelper @out)
    {
        this.@out = @out;
    }

    [Fact]
    public void Test1()
    {
        Reporter.CreateValueAndReferenceType(new Fixture(), this.@out);
    }
}

public class TestClass2
{
    private readonly ITestOutputHelper @out;

    public TestClass2(ITestOutputHelper @out)
    {
        this.@out = @out;
    }

    [Fact]
    public void Test2()
    {
        Reporter.CreateValueAndReferenceType(new Fixture(), this.@out);
    }
}

Когда вы запустите это с помощью средства запуска консоли xUnit.net, вы увидите хорошо воспроизведенную проблему:

$ packages/xunit.runner.console.2.1.0/tools/xunit.console 37925109/bin/Debug/Ploeh.StackOverflow.Q37925109.dll -diagnostics
-parallel all
xUnit.net Console Runner (64-bit .NET 4.0.30319.42000)
  Discovering: Ploeh.StackOverflow.Q37925109 (app domain = on [shadow copy], method display = ClassAndMethod)
  Discovered:  Ploeh.StackOverflow.Q37925109 (running 2 test cases)
  Starting:    Ploeh.StackOverflow.Q37925109 (parallel test collections = on, max threads = 4)
    Ploeh.StackOverflow.Q37925109.TestClass2.Test2 [PASS]
      Output:
        valueType: 246
        referenceType: cc39f570-046a-4a0a-8adf-ab7deadd0e26
    Ploeh.StackOverflow.Q37925109.TestClass1.Test1 [PASS]
      Output:
        valueType: 246
        referenceType: 87455351-03f7-4640-99fb-05af910da267
  Finished:    Ploeh.StackOverflow.Q37925109
=== TEST EXECUTION SUMMARY ===
   Ploeh.StackOverflow.Q37925109  Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0,429s

В приведенном выше примере вы заметите, что я явно вызвал бегун с помощью -parallel all, но мне не нужно было этого делать, так как это значение по умолчанию.

Если, с другой стороны, вы отключите распараллеливание с помощью -parallel none, вы увидите, что значения другие:

$ packages/xunit.runner.console.2.1.0/tools/xunit.console 37925109/bin/Debug/Ploeh.StackOverflow.Q37925109.dll -diagnostics
-parallel none
xUnit.net Console Runner (64-bit .NET 4.0.30319.42000)
  Discovering: Ploeh.StackOverflow.Q37925109 (app domain = on [shadow copy], method display = ClassAndMethod)
  Discovered:  Ploeh.StackOverflow.Q37925109 (running 2 test cases)
  Starting:    Ploeh.StackOverflow.Q37925109 (parallel test collections = off, max threads = 4)
    Ploeh.StackOverflow.Q37925109.TestClass2.Test2 [PASS]
      Output:
        valueType: 203
        referenceType: 1bc75a33-5542-4d9f-b42d-57ed85dc418d
    Ploeh.StackOverflow.Q37925109.TestClass1.Test1 [PASS]
      Output:
        valueType: 117
        referenceType: 6a508699-dc35-4bcd-8a7b-15eba64b24b4
  Finished:    Ploeh.StackOverflow.Q37925109
=== TEST EXECUTION SUMMARY ===
   Ploeh.StackOverflow.Q37925109  Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0,348s

Я думаю, что из-за параллелизма и Test1, и Test2 выполняются параллельно и, по сути, в одном и том же тике.

Один из обходных путей — поместить оба теста в один и тот же тестовый класс:

public class TestClass1
{
    private readonly ITestOutputHelper @out;

    public TestClass1(ITestOutputHelper @out)
    {
        this.@out = @out;
    }

    [Fact]
    public void Test1()
    {
        Reporter.CreateValueAndReferenceType(new Fixture(), this.@out);
    }

    [Fact]
    public void Test2()
    {
        Reporter.CreateValueAndReferenceType(new Fixture(), this.@out);
    }
}

Это дает два разных целочисленных значения, потому что (IIRC) xUnit.net запускает только разные тестовые классы параллельно:

$ packages/xunit.runner.console.2.1.0/tools/xunit.console 37925109/bin/Debug/Ploeh.StackOverflow.Q37925109.dll -diagnostics
-parallel all
xUnit.net Console Runner (64-bit .NET 4.0.30319.42000)
  Discovering: Ploeh.StackOverflow.Q37925109 (app domain = on [shadow copy], method display = ClassAndMethod)
  Discovered:  Ploeh.StackOverflow.Q37925109 (running 2 test cases)
  Starting:    Ploeh.StackOverflow.Q37925109 (parallel test collections = on, max threads = 4)
    Ploeh.StackOverflow.Q37925109.TestClass1.Test2 [PASS]
      Output:
        valueType: 113
        referenceType: e8c30ad8-f2c8-4767-9e9f-69b55c50e659
    Ploeh.StackOverflow.Q37925109.TestClass1.Test1 [PASS]
      Output:
        valueType: 232
        referenceType: 3eb60bf3-4d43-4a91-aef2-42f7e23e35b3
  Finished:    Ploeh.StackOverflow.Q37925109
=== TEST EXECUTION SUMMARY ===
   Ploeh.StackOverflow.Q37925109  Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0,360s

Эта теория также подтверждается тем фактом, что если вы повторите эксперимент достаточное количество раз, вы увидите, что время от времени числа будут отличаться. Вот целочисленные результаты 25 тестовых прогонов:

33  33
92  92
211 211
13  13
9   9
160 160
55  55
155 155
137 137
161 161
242 242
183 183
237 237
151 151
104 104
254 254
123 123
244 244
144 144
223 9
196 196
126 126
199 199
221 221
132 132

Обратите внимание, что все тестовые прогоны, кроме одного, имеют одинаковые номера.

person Mark Seemann    schedule 20.06.2016
comment
Обходной путь может быть следующим: this.Register‹int›(() =› (byte)Guid.NewGuid().GetHashCode()); - person Felix Liebrecht; 21.06.2016
comment
исправление моего предложения: this.Register‹int›(() =› Math.Max((byte)1, (byte)Guid.NewGuid().GetHashCode())); - person Felix Liebrecht; 21.06.2016