我正在尝试为以下代码编写一些单元测试
private bool _initCalled = false;
private void Initialize()
{
if (!_initCalled)
{
lock (this)
{
if (_initCalled)
return;
NoConfigInit();
_initCalled = true;
}
}
}
测试此代码以获取代码路径if (_initCalled) return
的最佳方法是什么?
表示此代码的其他方式也很受欢迎,但我很好奇如何测试这些模式的正确性。
答案 0 :(得分:1)
我有办法测试你编写的代码,但它有几个条件:
您需要使用Visual Studio的企业版才能使用 微软假装(你可能会得到类似的概念。) 有一个名为Prig的免费替代品,但我对它没有经验)
您必须定位.net框架,而不是.net核心。
我们必须稍微改变你的代码,如下:
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方法仅被调用一次。这不是必须的,您也可以用其他方式来做,但是在这种情况下,我认为这是最好的解决方案。