Модульный тест на потокобезопасность?



Я написал класс и много модульных тестов, но я не сделал его потокобезопасным. Теперь я хочу сделать класс потокобезопасным, но чтобы доказать это и использовать TDD, я хочу написать некоторые неудачные модульные тесты, прежде чем начать рефакторинг.



есть хороший способ сделать это?



моя первая мысль - просто создать пару потоков и заставить их все использовать класс небезопасным способом. Сделайте это достаточно раз с достаточным количеством нитей, и я обязательно увижу, как он сломается.

682   9  

9 ответов:

есть два продукта, которые могут помочь вам там:

оба проверяют наличие тупиков в вашем коде (через модульный тест), и я думаю, что шахматы также проверяют условия гонки.

использование обоих инструментов легко-вы пишете простой модульный тест и запускаете свой код несколько раз и проверяете, возможны ли взаимоблокировки / условия гонки в вашем код.

Edit: Google выпустила инструмент, который проверяет состояние гонки во время выполнения (не во время тестов), который называется thread-race-test.
он не найдет все условия гонки, потому что он анализирует только текущий запуск, а не все возможные сценарии, такие как инструмент выше, но это может помочь вам найти состояние гонки, как только это произойдет.

обновление: Сайт Typemock больше не имел ссылки на Racer, и его не было был обновлен в последние 4 года. Я думаю, что проект был закрыт.

проблема в том, что большинство многопоточных вопросов, таких как условия расы, не детерминированы по своей природе. Они могут зависеть от поведения оборудования, которое вы не можете эмулировать или запускать.

Это означает, что, даже если вы делаете тесты с несколькими потоками, они не будут последовательно, не если у вас есть дефект в коде.

обратите внимание, что ответ Dror явно не говорит об этом, но, по крайней мере, шахматы (и, вероятно, гонщик) работают, выполняя набор потоков через все их возможные перемежения, чтобы получить повторяемые ошибки. Они не просто запускают потоки на некоторое время, надеясь, что если есть ошибка, это произойдет по совпадению.

шахматы, например, будут проходить через все перемежения, а затем дать вам строку тега, которая представляет перемежение, что тупик был найден на так что вы можете отнесите свои тесты к определенным чередованиям, которые интересны с точки зрения взаимоблокировки.

Я не знаю точной внутренней работы этого инструмента, и как он отображает эти строки тегов обратно в код, который вы можете изменить, чтобы исправить тупик, но там у вас есть это... Я действительно с нетерпением жду, когда этот инструмент (и Pex) станет частью VS IDE.

Я видел, как люди пытаются проверить это со стандартными unittests, как вы сами предлагаете. Тесты медленно, и до сих пор не удалось выявить ни одной из проблем параллелизма наша компания борется с.

после многих неудач, и несмотря на мою любовь к unittests, я пришел к выводу, что ошибки в параллелизме не является одной из сильных сторон unittests. Я обычно поощряю анализ и обзор в пользу unittests для классов, где параллелизм является предметом. С общим обзором системы во многих случаях можно доказать / фальсифицировать претензии по безопасности потоков.

во всяком случае, я хотел бы, чтобы кто-то дал мне что-то, что может указывать на обратное, поэтому я внимательно слежу за этим вопросом.

когда мне недавно пришлось решать ту же проблему, я думал об этом таким образом; прежде всего, ваш существующий класс несет одну ответственность, и это должно обеспечить некоторую функциональность. Это не ответственность объектов, чтобы быть потокобезопасным. Если он должен быть потокобезопасным, для обеспечения этой функции следует использовать какой-либо другой объект. Но если какой-то другой объект обеспечивает потокобезопасность, он не может быть необязательным, потому что тогда вы не можете доказать, что ваш код является потокобезопасным. Так вот как я справляюсь это:

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

несмотря на то, что это реализация, я надеюсь, что вы можете выяснить тесты для проверки этих классов по мере необходимости.

testNG или Junit с тестовым модулем springframeworks (или другим расширением) имеет базовую поддержку для тестирования параллелизма.

эта ссылка может вас заинтересовать

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

вам придется построить тестовый случай для каждого сценария параллелизма; это может потребовать замены эффективных операций более медленными эквивалентами (или насмешками) и запуска нескольких тестов в циклах, чтобы увеличить вероятность конфликтов

без конкретных тестовых случаев трудно предложить конкретные тесты

некоторые потенциально полезные справочные материалы:

хотя это не так элегантно, как использование такого инструмента, как гонщик или шахматы, я использовал такие вещи для тестирования безопасности потоков:

// 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++;
    }
}

теория заключается в том, что если вы можете вызвать опасное состояние потока последовательно происходить в очень короткий промежуток времени, вы должны быть в состоянии вызвать потокобезопасные условия и проверить их, ожидая относительно большое количество времени без наблюдения коррупции состояния.

Я допускаю, что это может быть примитивный способ сделать это и не может помочь в сложных ситуациях.

вот мой подход. Этот тест не связан с тупиками, он связан с последовательностью. Я тестирую метод с синхронизированным блоком, с кодом, который выглядит примерно так:

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

трудно создать сценарий, который будет надежно создавать конфликт потоков, но вот что я сделал.

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, ваш тест действительный.

вот что я сделал, и мой тест не провалился, поэтому он не был (пока) действительным тестом. Но я смог надежно произвести сбой, поместив код выше внутри цикла и запустив его 100 раз подряд. Поэтому я вызываю метод 5000 раз. (Да, это произведет медленный тест. Не беспокойся об этом. Ваши клиенты не будут обеспокоены этим, так что Вы тоже не должны.)

Как только я поместил этот код внутри внешнего цикла, я смог надежно увидеть сбой о 20-й итерации внешнего цикла. Теперь я был уверен, что тест действителен, и я восстановил синхронизированные ключевые слова для запуска фактического теста. (Это сработало.)

Вы можете обнаружить, что тест действителен на одну машину, а не на другой. Если тест действителен на одной машине и ваши методы проходят тест, то он предположительно потокобезопасен на всех машинах. Но вы должны проверить правильность на машине, которая выполняет ваши ночные модульные тесты.

Comments

    Ничего не найдено.