单元测试c#双锁模式

时间:2017-06-15 06:55:23

标签: c# unit-testing

我正在尝试为以下代码编写一些单元测试

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

测试此代码以获取代码路径if (_initCalled) return的最佳方法是什么?

表示此代码的其他方式也很受欢迎,但我很好奇如何测试这些模式的正确性。

2 个答案:

答案 0 :(得分:1)

我有办法测试你编写的代码,但它有几个条件:

  1. 您需要使用Visual Studio的企业版才能使用 微软假装(你可能会得到类似的概念。) 有一个名为Prig的免费替代品,但我对它没有经验)

  2. 您必须定位.net框架,而不是.net核心。

  3. 我们必须稍微改变你的代码,如下:

    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()
        {
    
        }
     }
    

    然后,在生成库的假货之后,我们可以这样编写测试:

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

    如果您调试测试,您将看到它在第二次检查时退出。

    我同意这不是测试它的理想方式,因为我们必须修改原始代码。

    沿着同一行的另一个选择是将_initCalled更改为属性 - >然后Fakes可以挂钩到setter和getter,这样你就可以避免使用DummyMethod,只需在第二次调用时返回true,就像这样(在单元测试中):

         int calls = 0;
         Namespace.Fakes.ShimClass3.AllInstances.InitCalledGet = class3 => calls++ > 0;
    

答案 1 :(得分:0)

一种测试方法(没有Microsoft Fakes或类似产品,而是使用模拟框架,例如FakeItEasy,Moq或NSubstitute)是进行两个测试用例,一个测试用例中您同时调用了Initialize方法在两个不同的线程中测试锁内部的if语句,在一个测试用例中,您连续调用Initialize方法以测试锁外部的if语句。像这样:

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

第一次测试中的小延迟确保该方法实际上需要花费一些时间来执行,如果将其删除,或者如果延迟太小,则测试仍将变为绿色,但是这可能导致第二次执行有时将击中外部if语句,而不是内部。

如您所见,我还利用了依赖项注入来简化测试,因此noConfig.Init方法仅被调用一次。这不是必须的,您也可以用其他方式来做,但是在这种情况下,我认为这是最好的解决方案。