线程安全的单元测试?

时间:2009-11-11 15:13:29

标签: c# .net unit-testing nunit thread-safety

我已经编写了一个类和许多单元测试,但我没有让它保持线程安全。现在,我想让类线程安全,但为了证明它并使用TDD,我想在开始重构之前编写一些失败的单元测试。

有什么好方法吗?

我的第一个想法是创建一些线程并使它们以不安全的方式使用该类。用足够的线程做足够的时间,我一定会看到它破裂。

9 个答案:

答案 0 :(得分:21)

有两种产品可以帮助您:

两者都检查代码中的死锁(通过单元测试),我认为Chess也会检查竞争条件。

使用这两种工具很简单 - 您可以编写一个简单的单元测试并多次运行代码并检查代码中是否存在死锁/竞争条件。

修改 谷歌发布了一个工具,用于检查调用thread-race-test的运行时(而不是测试期间)的竞争条件 它不会找到所有竞争条件,因为它只分析当前的运行而不是像上面的工具那样的所有可能的情况,但它可能会帮助你找到竞争条件。

<强>更新 Typemock网站不再有到Racer的链接,并且在过去的4年里没有更新。我猜这个项目已经关闭了。

答案 1 :(得分:10)

问题在于,大多数多线程问题(如竞争条件)本质上都是不确定的。它们可能依赖于您无法模拟或触发的硬件行为。

这意味着,即使您使用多个线程进行测试,如果您的代码中存在缺陷,它们也不会一直失败。

答案 2 :(得分:5)

请注意,Dror的答案没有明确说明这一点,但至少Chess(可能还有Racer)通过运行一组线程来完成所有可能的交错来获得可重现的错误。他们不只是运行线程一段时间,希望如果有错误,它将巧合发生。

例如,Chess将遍历所有交错,然后为您提供一个标记字符串,表示发现死锁的交错,以便您可以使用从死锁视角中感兴趣的特定交错来归因您的测试。

我不知道这个工具的确切内部工作方式,以及它如何将这些标记字符串映射回您可能正在修改以修复死锁的代码,但是您有它...我实际上真的很期待这个工具(和Pex)成为VS IDE的一部分。

答案 3 :(得分:3)

我看到有人试图用你自己提出的标准单元测试来测试这个。测试速度很慢,到目前为止还未能确定我们公司遇到的一个并发问题。

经过多次失败,尽管我对单元测试充满热爱,但我已经开始接受并发性错误并不是单元测试的优势之一。我通常鼓励分析和审查,以支持并发是主题的类的单元测试。通过对系统的总体概述,在许多情况下可以证明/伪造线程安全声明。

无论如何,我希望有人给我一些可能指向相反的东西,所以我会密切关注这个问题。

答案 4 :(得分:2)

当我最近不得不解决同样的问题时,我就这样想了;首先,您现有的类有一个责任,那就是提供一些功能。线程安全不是对象的责任。如果需要线程安全,则应使用其他一些对象来提供此功能。但是,如果某个其他对象提供线程安全,则它不能是可选的,因为那时您无法证明您的代码是线程安全的。所以这就是我处理它的方式:

// This interface is optional, but is probably a good idea.
public interface ImportantFacade
{
    void ImportantMethodThatMustBeThreadSafe();
}

// This class provides the thread safe-ness (see usage below).
public class ImportantTransaction : IDisposable
{
    public ImportantFacade Facade { get; private set; }
    private readonly Lock _lock;

    public ImportantTransaction(ImportantFacade facade, Lock aLock)
    {
        Facade = facade;
        _lock = aLock;
        _lock.Lock();
    }

    public void Dispose()
    {
        _lock.Unlock();
    }
}

// I create a lock interface to be able to fake locks in my tests.
public interface Lock
{
    void Lock();
    void Unlock();
}

// This is the implementation I want in my production code for Lock.
public class LockWithMutex : Lock
{
    private Mutex _mutex;

    public LockWithMutex()
    {
        _mutex = new Mutex(false);
    }

    public void Lock()
    {
        _mutex.WaitOne();
    }

    public void Unlock()
    {
        _mutex.ReleaseMutex();
    }
}

// This is the transaction provider. This one should replace all your
// instances of ImportantImplementation in your code today.
public class ImportantProvider<T> where T:Lock,new()
{
    private ImportantFacade _facade;
    private Lock _lock;

    public ImportantProvider(ImportantFacade facade)
    {
        _facade = facade;
        _lock = new T();
    }

    public ImportantTransaction CreateTransaction()
    {
        return new ImportantTransaction(_facade, _lock);
    }
}

// This is your old class.
internal class ImportantImplementation : ImportantFacade
{
    public void ImportantMethodThatMustBeThreadSafe()
    {
        // Do things
    }
}

使用泛型可以在测试中使用假锁来验证在创建事务时始终采用锁定,而在处理事务之前不会释放锁定。现在,您还可以在调用重要方法时验证是否已执行锁定。生产代码中的用法应如下所示:

// Make sure this is the only way to create ImportantImplementation.
// Consider making ImportantImplementation an internal class of the provider.
ImportantProvider<LockWithMutex> provider = 
    new ImportantProvider<LockWithMutex>(new ImportantImplementation());

// Create a transaction that will be disposed when no longer used.
using (ImportantTransaction transaction = provider.CreateTransaction())
{
    // Access your object thread safe.
    transaction.Facade.ImportantMethodThatMustBeThreadSafe();
}

通过确保其他人无法创建ImportantImplementation(例如在提供程序中创建并使其成为私有类),您现在可以证明您的类是线程安全的,因为没有事务和事务就无法访问它始终在创建时获取锁定,并在处置时释放它。

确保事务处理正确可能更难,如果不是,您可能会在应用程序中看到奇怪的行为。您可以使用工具作为Microsoft Chess(如另一个anser中所建议的)来查找类似的东西。或者您可以让您的提供者实现外观并使其实现如下:

    public void ImportantMethodThatMustBeThreadSafe()
    {
        using (ImportantTransaction transaction = CreateTransaction())
        {
            transaction.Facade.ImportantMethodThatMustBeThreadSafe();
        }
    }

即使这是实现,我希望你可以根据需要找出测试来验证这些类。

答案 5 :(得分:1)

带有springframeworks测试模块(或其他扩展)的testNG或Junit对并发测试有基本支持。

此链接可能会让您感兴趣

http://www.cs.rice.edu/~javaplt/papers/pppj2009.pdf

答案 6 :(得分:1)

你必须为每个关注的并发场景构建一个测试用例;这可能需要用较慢的等效(或模拟)替换有效的操作并在循环中运行多个测试,以增加争用的可能性

没有特定的测试用例,很难提出具体的测试

一些可能有用的参考资料:

答案 7 :(得分:1)

虽然它不像使用Racer或Chess这样的工具那么优雅,但我已经使用这种方法来测试线程安全性:

// from linqpad

void Main()
{
    var duration = TimeSpan.FromSeconds(5);
    var td = new ThreadDangerous(); 

    // no problems using single thread (run this for as long as you want)
    foreach (var x in Until(duration))
        td.DoSomething();

    // thread dangerous - it won't take long at all for this to blow up
    try
    {           
        Parallel.ForEach(WhileTrue(), x => 
            td.DoSomething());

        throw new Exception("A ThreadDangerException should have been thrown");
    }
    catch(AggregateException aex)
    {
        // make sure that the exception thrown was related
        // to thread danger
        foreach (var ex in aex.Flatten().InnerExceptions)
        {
            if (!(ex is ThreadDangerException))
                throw;
        }
    }

    // no problems using multiple threads (run this for as long as you want)
    var ts = new ThreadSafe();
    Parallel.ForEach(Until(duration), x => 
        ts.DoSomething());      

}

class ThreadDangerous
{
    private Guid test;
    private readonly Guid ctrl;

    public void DoSomething()
    {           
        test = Guid.NewGuid();
        test = ctrl;        

        if (test != ctrl)
            throw new ThreadDangerException();
    }
}

class ThreadSafe
{
    private Guid test;
    private readonly Guid ctrl;
    private readonly object _lock = new Object();

    public void DoSomething()
    {   
        lock(_lock)
        {
            test = Guid.NewGuid();
            test = ctrl;        

            if (test != ctrl)
                throw new ThreadDangerException();
        }
    }
}

class ThreadDangerException : Exception 
{
    public ThreadDangerException() : base("Not thread safe") { }
}

IEnumerable<ulong> Until(TimeSpan duration)
{
    var until = DateTime.Now.Add(duration);
    ulong i = 0;
    while (DateTime.Now < until)
    {
        yield return i++;
    }
}

IEnumerable<ulong> WhileTrue()
{
    ulong i = 0;
    while (true)
    {
        yield return i++;
    }
}

理论上说,如果你可以在很短的时间内导致线程危险情况一直发生,你应该能够带来线程安全的条件并通过等待相对较长的时间来验证它们而不需要观察状态损坏。

我承认这可能是一种处理它的原始方式,在复杂的情况下可能无济于事。

答案 8 :(得分:0)

这是我的方法。这个测试不关心死锁,而是关注一致性。我正在使用synchronized块测试一个方法,代码看起来像这样:

synchronized(this) {
  int size = myList.size();
  // do something that needs "size" to be correct,
  // but which will change the size at the end.
  ...
}

很难产生可靠地产生线程冲突的场景,但这就是我所做的。

首先,我的单元测试创​​建了50个线程,同时启动它们,并让它们都调用我的方法。我使用CountDown Latch同时启动它们:

CountDownLatch latch = new CountDownLatch(1);
for (int i=0; i<50; ++i) {
  Runnable runner = new Runnable() {
    latch.await(); // actually, surround this with try/catch InterruptedException
    testMethod();
  }
  new Thread(runner, "Test Thread " +ii).start(); // I always name my threads.
}
// all threads are now waiting on the latch.
latch.countDown(); // release the latch
// all threads are now running the test method at the same time.

这可能会也可能不会产生冲突。如果发生冲突,我的testMethod()应​​该能够抛出异常。但我们还不能确定这会产生冲突。所以我们不知道测试是否有效。所以这就是诀窍:注释掉你的synchronized关键字并运行测试。如果这会产生冲突,测试将失败。 如果在没有synchronized关键字的情况下失败,则表示您的测试有效。

这就是我所做的,我的测试没有失败,所以它还没有(有效)测试。但是我能够通过将上面的代码放在循环中并连续运行100次来可靠地产生故障。所以我称这个方法5000次。 (是的,这会产生一个缓慢的测试。不要担心。你的客户不会为此烦恼,所以你也不应该。)

一旦我把这个代码放在外部循环中,我能够可靠地看到外循环的第20次迭代失败。现在我确信测试有效,并且我恢复了synchronized关键字以运行实际测试。 (它奏效了。)

您可能会发现测试在一台计算机上有效,而在另一台计算机上无效。如果测试在一台机器上有效并且您的方法通过了测试,那么它可能在所有机器上都是线程安全的。但是你应该在运行夜间单元测试的机器上测试有效性。