【问题标题】:Unit test for race condition竞争条件的单元测试
【发布时间】:2016-11-04 18:04:20
【问题描述】:

考虑代码:

class TestClass
{
    private bool _someFlag;
    private object _sharedObject = new object();
    private readonly object _syncObject = new object();

    public object Read()
    {
        //lock (_syncObject)
        {
            _someFlag = false;
            return _sharedObject;
        }
    }

    public void Write(object obj)
    {
        //lock (_syncObject)
        {
            _someFlag = true;
            _sharedObject = obj;
        }
    }
}

它有竞争条件问题。当我们调用Read() 时,某些线程可以在_someFlag = false;return _sharedObject; 行之间调用Write()。我将通过lock 运营商解决问题。但是你能帮我对这个竞争条件问题进行单元测试吗?

我不想将 _someFlag 更改为 public 以进行测试或类似的事情。 我想做这样的事情:

[Fact]
public void RaceConditionTest()
{
    var correctObject = new object();
    var test = new TestClass();

    for (int i = 0; i < 1000; i++)
    {
        test.Write(correctObject);
        var assertTask = Task.Run(() =>
        {
            var actualObj = test.Read();
            Assert.True(object.ReferenceEquals(correctObject, actualObj), $"Failed on {i} iteration");
        });
        //Thread.Sleep(50);
        var failTask = Task.Run(() => test.Write(new object()));

        Task.WaitAll(assertTask, failTask);
    }
}

但是我怎么能确定assertTask 会在failTask 之前启动呢?或者也许还有另一种方法可以对这种情况进行单元测试?提前致谢。

【问题讨论】:

  • 竞争条件可能无法进行单元测试,无论是正面还是尤其是负面。为了使代码可靠地执行您不想要的操作,您必须专门设置同步机制,强制操作以可靠的顺序发生,以便您可以可靠地重现不良行为。但是要添加同步以可靠地产生不良行为,您现在强制代码行为错误;您可以轻松地编写它以使其行为正确,如果您不确定是否有,则不能依赖测试。

标签: c# multithreading unit-testing thread-safety race-condition


【解决方案1】:

要测试并发问题,最好让所有线程在定义的点等待,然后将它们全部释放。

var threadCount = someList.Length;
var threadsReady = new ManualResetEvent(false);
var threadsReadyCount = 0;   
Task.WhenAll(someList.Select(item => Task.Run(() => worker())));

void worker() {
    Interlocked.Increment(ref threadsReadyCount); 
    if (threadsReadyCount == threadCount)
        threadsReady.Set();
    else
        threadsReady.WaitOne();
    // Do some work...
}



         

【讨论】:

  • 您可以将ManualResetEvent+threadsReadyCount-counter 替换为CountdownEvent
【解决方案2】:

您可以在启动 failTask​​ 之前检查 assertTask 是否正在运行或已完成:

while (assertTask.Status != Running && assertTask.Status != RanToCompletion)
Thread.Sleep(50);

【讨论】:

  • 这对测试竞态条件有何帮助?我需要同时执行这两个任务。如果我已完成 assertTask,我将无法重现竞争条件。
  • “我如何确定 assertTask 将在failTask​​ 之前启动”的答案是像我上面建议的那样检查任务状态。没有保证重现竞争条件的方法。你只能通过多次运行这个循环来重现,超过 1000 次
【解决方案3】:

我一直坚持这种方法。但是还在寻找更好的方法... 此测试在某些迭代中失败,但如果您取消注释 lock 运算符,则测试将通过。

class TestClass
{
    private IEventRecorder _eventRecorder;


    private bool _someFlag;
    private object _sharedObject = new object();
    private readonly object _syncObject = new object();

#if DEBUG
    public void SetEventRecorder(IEventRecorder eventRecorder) => _eventRecorder = eventRecorder;
#endif

    public object Read()
    {
        //lock (_syncObject)
        {
#if DEBUG
            _eventRecorder?.Record(nameof(Read));
#endif
            _someFlag = false;
            return _sharedObject;
        }
    }

    public void Write(object obj)
    {
        //lock (_syncObject)
        {
#if DEBUG
            _eventRecorder?.Record(nameof(Write));
#endif
            _someFlag = true;
            _sharedObject = obj;
        }
    }

    public interface IEventRecorder
    {
        void Record(string eventName);
    }
}

public class TestClassTests
{
    private class EventRecorder : TestClass.IEventRecorder
    {
        private string _events = string.Empty;

        public void Record(string eventName) => _events += eventName;

        public string Events => _events;

        public void Reset() => _events = string.Empty;
    }

    [Fact]
    public void RaceConditionTest()
    {
        var correctObject = new object();
        var eventRecorder = new EventRecorder();
        var test = new TestClass();
        test.SetEventRecorder(eventRecorder);

        for (int i = 0; i < 1000; i++)
        {
            test.Write(correctObject);
            var assertTask = Task.Run(() =>
            {
                var actualObj = test.Read();
                if (eventRecorder.Events.StartsWith("WriteRead"))
                    Assert.True(object.ReferenceEquals(correctObject, actualObj), $"Failed on {i} iteration");
            });
            var failTask = Task.Run(() => test.Write(new object()));

            Task.WaitAll(assertTask, failTask);
            eventRecorder.Reset();
        }
    }
}

【讨论】:

    猜你喜欢
    • 2011-01-02
    • 2018-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-20
    • 2017-02-11
    相关资源
    最近更新 更多