Estoy experimentando escribiendo algunas pruebas unitarias para el siguiente código

private bool _initCalled = false;
private void Initialize()
{
    if (!_initCalled)
    {
        lock (this)
        {
            if (_initCalled)
                return;
            NoConfigInit();
            _initCalled = true;
        }
    }
}

¿Cuál es la mejor manera de probar este código para acceder a la ruta del código if (_initCalled) return?

También son bienvenidas formas alternativas de representar este código, pero tengo curiosidad acerca de cómo probar estos patrones para verificar que sean correctos.

1
H Bellamy 15 jun. 2017 a las 09:55

2 respuestas

La mejor respuesta

Tengo una forma de probar el código que escribiste, pero tiene un par de condiciones:

  1. Debe usar la edición Enterprise de Visual Studio para usar Microsoft Fakes (es posible que pueda hacer funcionar un concepto similar con la alternativa gratuita llamada Prig, pero no tengo experiencia con ella)

  2. Tienes que apuntar a .NET Framework, no a .NET Core.

Tenemos que alterar un poco su código, así:

public class Class3
  {
    private bool _initCalled = false;
    public void Initialize()
    {
      if (!_initCalled)
      {
        lock (this)
        {
          // we need to insert an empty DummyMethod here
          DummyMethod();
          if (_initCalled)
            return;
          NoConfigInit();
          _initCalled = true;
        }
      }
    }

    private void DummyMethod()
    {
      // This method stays empty in production code.
      // It provides the hook for the unit test.
    }

    private void NoConfigInit()
    {

    }
 }

Luego, después de generar las falsificaciones para la biblioteca, podemos escribir la prueba así:

[TestMethod]
public void TestMethod1()
{
  using (ShimsContext.Create())
  {
    // This is the value to capture whether the NoConfigInit was called
    var initCalled = false;

    // Here the DummyMethod comes into play
    Namespace.Fakes.ShimClass3.AllInstances.DummyMethod =
      class3 =>
        typeof(Class3).GetField("_initCalled", BindingFlags.Instance | BindingFlags.NonPublic)
          .SetValue(class3, true);

    // This is just a hook to check whether the NoConfigInit is called.
    // You may be able to test this using some other ways (e.g. asserting on state etc.)
    Namespace.Fakes.ShimClass3.AllInstances.NoConfigInit = class3 => initCalled = true;

    // act
    new Class3().Initialize();

    // assert
    Assert.IsFalse(initCalled);
  }
}

Si depura la prueba, verá que sale en la segunda verificación.

Estoy de acuerdo en que esta no es una forma ideal de probarlo, ya que tuvimos que modificar el código original.

Otra opción en la misma línea es cambiar _initCalled para que sea una propiedad -> luego las falsificaciones pueden conectarse a establecedores y captadores para que pueda evitar el DummyMethod, y simplemente devolver verdadero en la segunda llamada, así (en la prueba unitaria ):

     int calls = 0;
     Namespace.Fakes.ShimClass3.AllInstances.InitCalledGet = class3 => calls++ > 0;
1
zaitsman 16 jun. 2017 a las 12:17

Una forma de probar esto (sin Microsoft Fakes o similar, usando un marco de imitación en su lugar, es decir, FakeItEasy, Moq o NSubstitute) es hacer dos casos de prueba, un caso de prueba en el que llame al método Initialize al mismo tiempo en dos diferentes subprocesos para probar la declaración if dentro del bloqueo, y un caso de prueba donde se llama al método Initialize consecutivamente para probar la declaración if fuera del bloqueo. Algo como esto:

using System.Threading.Tasks;
using FakeItEasy;
using Xunit;

namespace MyNamespace
{
    public class MyClass
    {
        private readonly object _lock = new object();
        private readonly IMyInterface _noConfig;
        private bool _initCalled;

        public MyClass(IMyInterface noConfig)
        {
            _noConfig = noConfig;
        }

        public void Initialize()
        {
            if (_initCalled)
            {
                return;
            }

            lock (_lock)
            {
                if (_initCalled)
                {
                    return;
                }

                _noConfig.Init();
                _initCalled = true;
            }
        }
    }

    public interface IMyInterface
    {
        void Init();
    }

    public class MyTests
    {
        private readonly IMyInterface _myInterface;
        private readonly MyClass _myClass;

        public MyTests()
        {
            _myInterface = A.Fake<IMyInterface>();
            _myClass = new MyClass(_myInterface);
        }

        [Fact]
        public async void Initialize_CallConcurrently_InitNoConfigOnlyCalledOnce()
        {
            A.CallTo(() => _myInterface.Init()).Invokes(() => Thread.Sleep(TimeSpan.FromMilliseconds(5)));

            Task[] tasks =
            {
                Task.Run(() => _myClass.Initialize()),
                Task.Run(() => _myClass.Initialize())
            };
            await Task.WhenAll(tasks);

            A.CallTo(() => _myInterface.Init()).MustHaveHappenedOnceExactly();
        }

        [Fact]
        public void Initialize_CallConsecutively_InitNoConfigOnlyCalledOnce()
        {
            _myClass.Initialize();
            _myClass.Initialize();

            A.CallTo(() => _myInterface.Init()).MustHaveHappenedOnceExactly();
        }
    }
}

El pequeño retraso en la primera prueba asegura que el método realmente demore un tiempo en ejecutarse; si lo elimina o si el retraso es demasiado bajo, la prueba seguirá siendo verde, pero puede dar lugar a que la segunda ejecución a veces llegue al declaración if externa en lugar de la interna.

Como puede ver, también utilicé la inyección de dependencia para facilitar la prueba, por lo que el método noConfig.Init solo se llama una vez. Esto no es necesario, también puede hacerlo de otras maneras, pero en este caso pensé que era la mejor solución.

0
Martin Odhelius 19 jun. 2019 a las 14:55