none
UnitTest и многопоточное приложение RRS feed

  • Вопрос

  • Всем привет. Подскажите как правильно написать unit-тест для многопоточного приложения?

    В качестве примера хочу привести следующий код:

    private const int cipherStrength = 2048;
    
        /// <summary>
        ///A test for RSACrypto
        ///</summary>
        [TestMethod()]
        public void RSACryptoEncryptedAndDecryptedTest()
        {
          UnicodeEncoding ByteConverter = new UnicodeEncoding();
    
          string beginData = "Data to Encrypt";
          string result;
          byte[] dataToEncrypt = ByteConverter.GetBytes(beginData);
          byte[] encryptedData;
          byte[] decryptedData;
    
          RSACrypto RSA = new RSACrypto(cipherStrength);
    
          ManualResetEvent manualResetEvent = new ManualResetEvent(false);
    
          RSA.OnKeysGenerated += (sender) => manualResetEvent.Set(); 
          
          /// Проблема в том, что ключ генерируется в нескольких потоках
          RSA.GenerateKeys();
    
          /// Попытка подождать, когда колючик будет сгенерирован, воспринимается студией - что тест отменен
          Assert.IsTrue(manualResetEvent.WaitOne(10000));
          
          RSAParameters publicKey = RSA.ExportParameters(false);
          RSAParameters privateKey = RSA.ExportParameters(true);
    
          encryptedData = RSAEncrypt(dataToEncrypt, publicKey);
          decryptedData = RSADecrypt(encryptedData, privateKey);
    
          result = ByteConverter.GetString(decryptedData);
          
          Assert.AreEqual(beginData, result);
        }
    
        /// <summary>
        /// Функция для шифрования
        /// </summary>
        /// <param name="DataToEncrypt"></param>
        /// <param name="RSAKeyInfo"></param>
        /// <returns></returns>
        static public byte[] RSAEncrypt(byte[] DataToEncrypt, RSAParameters RSAKeyInfo)
        {
          byte[] encryptedData;
    
          RSACrypto RSA = new RSACrypto(cipherStrength);
          RSA.ImportParameters(RSAKeyInfo);
    
          encryptedData = RSA.Encrypt(DataToEncrypt);
    
          return encryptedData;
        }
    
        /// <summary>
        ///Функция для дешифрования
        /// </summary>
        /// <param name="DataToDecrypt"></param>
        /// <param name="RSAKeyInfo"></param>
        /// <returns></returns>
        static public byte[] RSADecrypt(byte[] DataToDecrypt, RSAParameters RSAKeyInfo)
        {
          byte[] decryptedData;
          
          RSACrypto RSA = new RSACrypto(cipherStrength);
    
          RSA.ImportParameters(RSAKeyInfo);
          decryptedData = RSA.Decrypt(DataToDecrypt);
    
          return decryptedData;
        }
    19 февраля 2011 г. 12:10

Ответы

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

    Но тем не менее, согласно классическим принципам модульного тестирования, тест должен проверять функционал в отрыве от внешней среды. Многопоточность в данном случае — это как раз внешняя среда.

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

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

    • Помечено в качестве ответа DenisX555 19 февраля 2011 г. 14:19
    19 февраля 2011 г. 13:26
  • Я бы не согласился. Код, который создает и запускает потоки, можно и нужно проверять юнит-тестами. Выбросить проверку на шифрование/дешифрование, и оставить тест "вызов GenerateKeys привозит к вызову OnKeysGenerated в течении 10 секунд". GenerateKeys - вполне так unit. Тест, проверяющий его - unit test. Тест, проверющий что при исключении метод не вызывается - тоже unit-тест.

    Многопоточноть - не "внешняя среда", по крайней мере для вашего кода. Наверняка там внутри используются просто потоки из пула, или есть ручное управление потоками, никак не пересекающееся с внешним кодом. Да, если внутри используется действительно несколько пересекающихся потоков - то проверка будет неполной, даже при полном покрытии кода. Но это опять же не повод не писать тесты вообще.

    А иначе вы получите "100% покрытый юнит-тестами код", который даже ключи сгенерировать не может.


    My blog
    • Помечено в качестве ответа DenisX555 19 февраля 2011 г. 15:31
    19 февраля 2011 г. 14:41
    Модератор
  • Тест написан вполне корректно. А что происходит при выполнении? Первый Assert.IsTrue падает через 10 секунд? Значит или ключи не успевают сгенерироваться, или при генерации происходит ошибка, и событие OnKeysGenerated не вызывается. В любом случае нужно отладить и исправить. Без исходного кода RSACrypto ничего определенного сказать нельзя.
    My blog


    Извеняюсь за беспокойство, все работает. Проблема была не в тесте! Просто в одном из потоков возникал Exception. Только в результах теста он конечно же не отображался...

    Кстати хочу привести другой пример кода по заданому вопросу, и который работает:

    public void Test()
     {
      ManualResetEvent manualResetEvent = new ManualResetEvent(false);
    
      System.Timers.Timer timer = new System.Timers.Timer();
      timer.Elapsed += (object sender, ElapsedEventArgs e) =>
      {
       System.Timers.Timer t = (System.Timers.Timer)sender;
       t.Enabled = false;
    
       manualResetEvent.Set();
      };
      timer.Interval = 1000; //Устанавливаем интервал в 1 сек.
      timer.Enabled = true; //Вкючаем таймер.
    
      Assert.IsTrue(manualResetEvent.WaitOne(10000)); /// Ждем когда сработет таймер
      Assert.IsFalse(timer.Enabled);
    }
    

     

    • Помечено в качестве ответа DenisX555 19 февраля 2011 г. 14:18
    19 февраля 2011 г. 14:11

Все ответы

  • Обычно, корректность многопоточного выполнения не проверяется модульными тестами, вместо этого, зачастую, используется code review.

    Из инструментов Вы можете посмотреть TypeMock Racer (пока Beta, в релизе, скорее всего, будет платным), который пытается найти возможные проблемы, связанные с многопоточностью (но 100% гарантии отсутствия ошибок не дает).

    В крайнем случае Вы можете написать собственные тесты, которые запускают некоторое переменное количество потоков обработки с разными задержками и выполняют их, скажем, всю ночь, до тех пор, пока Вы не придете на работу. Но это не позволит выявить редковстречающиеся проблемы и проблемы, связанные со спецификой влияния на выполнение другими приложениями, запущенными на компьютере. Так что единственный более-менее надежный вариант — ручная проверка кода.

    19 февраля 2011 г. 12:39
  • Обычно, корректность многопоточного выполнения не проверяется модульными тестами, вместо этого, зачастую, используется code review.

    Из инструментов Вы можете посмотреть TypeMock Racer (пока Beta, в релизе, скорее всего, будет платным), который пытается найти возможные проблемы, связанные с многопоточностью (но 100% гарантии отсутствия ошибок не дает).

    В крайнем случае Вы можете написать собственные тесты, которые запускают некоторое переменное количество потоков обработки с разными задержками и выполняют их, скажем, всю ночь, до тех пор, пока Вы не придете на работу. Но это не позволит выявить редковстречающиеся проблемы и проблемы, связанные со спецификой влияния на выполнение другими приложениями, запущенными на компьютере. Так что единственный более-менее надежный вариант — ручная проверка кода.


    В моем случае я не проверяю "корректность многопоточного выполнения" мне просто нужно проверить функции шифрования и дешифрования.

    19 февраля 2011 г. 12:47
  • Тест написан вполне корректно. А что происходит при выполнении? Первый Assert.IsTrue падает через 10 секунд? Значит или ключи не успевают сгенерироваться, или при генерации происходит ошибка, и событие OnKeysGenerated не вызывается. В любом случае нужно отладить и исправить. Без исходного кода RSACrypto ничего определенного сказать нельзя.
    My blog
    19 февраля 2011 г. 13:10
    Модератор
  • Теперь, когда Вы обновили текст вопроса, я вижу, что Вы спрашивали о другом.

    Но тем не менее, согласно классическим принципам модульного тестирования, тест должен проверять функционал в отрыве от внешней среды. Многопоточность в данном случае — это как раз внешняя среда.

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

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

    • Помечено в качестве ответа DenisX555 19 февраля 2011 г. 14:19
    19 февраля 2011 г. 13:26
  • Тест написан вполне корректно. А что происходит при выполнении? Первый Assert.IsTrue падает через 10 секунд? Значит или ключи не успевают сгенерироваться, или при генерации происходит ошибка, и событие OnKeysGenerated не вызывается. В любом случае нужно отладить и исправить. Без исходного кода RSACrypto ничего определенного сказать нельзя.
    My blog


    Извеняюсь за беспокойство, все работает. Проблема была не в тесте! Просто в одном из потоков возникал Exception. Только в результах теста он конечно же не отображался...

    Кстати хочу привести другой пример кода по заданому вопросу, и который работает:

    public void Test()
     {
      ManualResetEvent manualResetEvent = new ManualResetEvent(false);
    
      System.Timers.Timer timer = new System.Timers.Timer();
      timer.Elapsed += (object sender, ElapsedEventArgs e) =>
      {
       System.Timers.Timer t = (System.Timers.Timer)sender;
       t.Enabled = false;
    
       manualResetEvent.Set();
      };
      timer.Interval = 1000; //Устанавливаем интервал в 1 сек.
      timer.Enabled = true; //Вкючаем таймер.
    
      Assert.IsTrue(manualResetEvent.WaitOne(10000)); /// Ждем когда сработет таймер
      Assert.IsFalse(timer.Enabled);
    }
    

     

    • Помечено в качестве ответа DenisX555 19 февраля 2011 г. 14:18
    19 февраля 2011 г. 14:11
  • Теперь, когда Вы обновили текст вопроса, я вижу, что Вы спрашивали о другом.

    Но тем не менее, согласно классическим принципам модульного тестирования, тест должен проверять функционал в отрыве от внешней среды. Многопоточность в данном случае — это как раз внешняя среда.

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

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


    Согласен. Спасибо за помощь.
    • Помечено в качестве ответа DenisX555 19 февраля 2011 г. 14:19
    • Снята пометка об ответе DenisX555 19 февраля 2011 г. 14:19
    19 февраля 2011 г. 14:18
  • Я бы не согласился. Код, который создает и запускает потоки, можно и нужно проверять юнит-тестами. Выбросить проверку на шифрование/дешифрование, и оставить тест "вызов GenerateKeys привозит к вызову OnKeysGenerated в течении 10 секунд". GenerateKeys - вполне так unit. Тест, проверяющий его - unit test. Тест, проверющий что при исключении метод не вызывается - тоже unit-тест.

    Многопоточноть - не "внешняя среда", по крайней мере для вашего кода. Наверняка там внутри используются просто потоки из пула, или есть ручное управление потоками, никак не пересекающееся с внешним кодом. Да, если внутри используется действительно несколько пересекающихся потоков - то проверка будет неполной, даже при полном покрытии кода. Но это опять же не повод не писать тесты вообще.

    А иначе вы получите "100% покрытый юнит-тестами код", который даже ключи сгенерировать не может.


    My blog
    • Помечено в качестве ответа DenisX555 19 февраля 2011 г. 15:31
    19 февраля 2011 г. 14:41
    Модератор
  • Напиши тест, который проверяет, что Exception ошибки во время генерации ключей доходит до вызвавшего кода (хотя бы в виде события). :)

    My blog
    19 февраля 2011 г. 14:45
    Модератор
  • тест "вызов GenerateKeys привозит к вызову OnKeysGenerated в течении 10 секунд"

    В такой формулировке — да, согласен. Если это является частью запланированного функционала. И про 100%-покрытие тоже согласен.

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

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

    19 февраля 2011 г. 17:56