Як я маю одиницю тестового потокового коду?


704

Я поки що уникав кошмару, який тестує багатопотоковий код, оскільки він просто здається занадто великим мінним полем. Мені хотілося б запитати, як люди пішли на тестування коду, який спирається на теми для успішного виконання, або як люди почали тестувати такі проблеми, які з’являються лише тоді, коли два потоки взаємодіють певним чином?

Це здається дійсно ключовою проблемою для програмістів сьогодні, було б корисно об'єднати наші знання з цього імхо.


2
Я думав поставити запитання з цього самого питання. Хоча Вілл робить багато хороших моментів нижче, я думаю, що ми можемо зробити краще. Я згоден, немає єдиного "підходу" до вирішення цього питання чисто. Однак "тестування якнайкраще" встановлює планку дуже низькою. Повернусь зі своїми висновками.
Zach Burlingame

У Java: Пакет java.util.concurrent містить деякі погано відомі класи, які можуть допомогти писати детерміновані тести JUnit. Подивіться - CountDownLatch - Семафор - Обмінник
Synox

Чи можете ви надати посилання на попереднє запитання, пов'язане з тестуванням одиниць?
Ендрю Грімм


7
Я думаю, що важливо відзначити, що цьому питанню 8 років, а бібліотеки додатків тим часом пройшли досить довгий шлях. У «сучасну епоху» (2016) багатопотокова розробка приходить головним чином у вбудованих системах. Але якщо ви працюєте на настільному або телефонному додатку, спочатку вивчіть альтернативи. Навколишні додатки, такі як .NET, тепер включають інструменти для управління або значно спрощення, ймовірно, 90% загальних сценаріїв багаторівневої нитки. (asnync / wait, PLinq, IObservable, TPL ...). Багатопотоковий код важкий. Якщо ви не винаходите колесо, вам не доведеться повторно його перевіряти.
Пол Вільямс

Відповіді:


245

Подивіться, немає простого способу зробити це. Я працюю над проектом, який по суті є багатопоточним. Події надходять з операційної системи, і я повинен їх одночасно обробляти.

Найпростіший спосіб впоратися з тестуванням складного, багатопотокового коду програми: Це якщо його занадто складний для тестування, ви робите це неправильно. Якщо у вас є один екземпляр, який має на ньому кілька потоків, і ви не можете перевірити ситуації, коли ці потоки переходять один на одного, то ваш дизайн потрібно переробити. Це настільки ж просто і настільки ж складно.

Існує багато способів програмування для багатопотокового прочитання, що дозволяє уникнути одночасного запуску ниток через екземпляри. Найпростіше - зробити всі свої об'єкти непорушними. Звичайно, це зазвичай неможливо. Отже, ви повинні визначити ті місця у вашому дизайні, де потоки взаємодіють з одним і тим же екземпляром і зменшити кількість цих місць. Роблячи це, ви виділяєте кілька класів, де насправді відбувається багатопоточність, зменшуючи загальну складність тестування вашої системи.

Але ви повинні усвідомити, що навіть роблячи це, ви все ще не можете перевірити кожну ситуацію, коли дві нитки наступають одна на одну. Для цього вам доведеться одночасно запускати два потоки в одному тесті, а потім точно контролювати, які рядки вони виконують у будь-який момент. Найкраще, що ви можете зробити, це імітувати цю ситуацію. Але для цього може знадобитися кодування спеціально для тестування, і це в кращому випадку півкроку до справжнього рішення.

Напевно, найкращий спосіб перевірити код для випусків ниток - це статичний аналіз коду. Якщо ваш потоковий код не відповідає обмеженому набору безпечних моделей потоків, то у вас може виникнути проблема. Я вважаю, що аналіз коду у VS містить певні знання з нарізання різьби, але, мабуть, не дуже.

Подивіться, як зараз справи стоять (і, ймовірно, будуть стояти гарний час), найкращий спосіб перевірити багатопотокові програми - максимально зменшити складність потокового коду. Мінімізуйте області, де взаємодіють потоки, протестуйте якнайкраще та використовуйте аналіз коду для виявлення небезпечних зон.


1
Аналіз коду чудово, якщо ви маєте справу з мовою / рамкою, яка це дозволяє. EG: Findbugs знайде дуже прості та легкі проблеми спільної сумісності зі статичними змінними. Те, що він не може знайти, є однотонними шаблонами дизайну, але передбачає, що всі об'єкти можна створити кілька разів. Цей плагін надзвичайно неадекватний для таких рамок, як Spring.
Зомбі

3
насправді є ліки: активні предмети. drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…
Кріп

6
Хоча це гарна порада, я все ще залишаюсь запитати: "як я перевіряю ті мінімальні області, де потрібно кілька потоків?"
Брайан Рейнер

5
"Якщо це занадто складно для тестування, ви робите це неправильно", - ми всі повинні зануритися в застарілий код, який ми не писали. Як це спостереження допомагає комусь саме?
Рона

2
Статичний аналіз, ймовірно, є хорошою ідеєю, але це не тестування. Цей пост насправді не відповідає на питання, яке стосується тестування.
Warren Dew

96

Минув час, коли це питання було розміщено, але на нього все ще не було відповіді ...

Відповідь kleolb02 - хороша відповідь. Я спробую розібратися більше деталей.

Є спосіб, який я практикую для C # коду. Для одиничних тестів ви повинні мати змогу запрограмувати відтворювані тести, що є найбільшою проблемою в багатопотоковому коді. Тому моя відповідь спрямована на те, щоб примусити асинхронний код у тестовому джгуті, який працює синхронно .

Це ідея з книги Джерара Мезардоса " xUnit Test Patterns " і називається "Humble Object" (стор. 695): Ви повинні відокремити основний логічний код і все, що пахне асинхронним кодом один від одного. Це призведе до класу для основної логіки, який працює синхронно .

Це ставить вас у змогу синхронно перевірити основний логічний код . Ви маєте абсолютний контроль над термінами дзвінків, які ви робите за основними логіками, і таким чином можете робити повторювані тести. І це ваш прибуток від розмежування основної логіки та асинхронної логіки.

Цю основну логіку потрібно обгорнути іншим класом, який відповідає за асинхронний прийом викликів до основної логіки і делегує ці виклики основній логіці. Виробничий код матиме доступ до основної логіки лише через цей клас. Оскільки цей клас повинен делегувати лише дзвінки, це дуже "німий" клас без особливої ​​логіки. Таким чином, ви можете мінімізувати свої одиничні тести для цього асихронного робочого класу.

Все вище, ніж тестування (тестування взаємодії між класами) - це складові тести. Також у цьому випадку вам слід мати можливість абсолютного контролю над тимчасовим часом, якщо ви будете дотримуватися шаблону «Скірний об’єкт».


1
Але іноді, якщо нитки добре співпрацюють між собою, теж щось слід перевірити, правда? Однозначно я буду відокремлювати основну логіку від асинхронної частини, прочитавши вашу відповідь. Але я все ще буду перевіряти логіку за допомогою інтерфейсів асинхронізації із зворотним зворотом робота на всіх потоках.
CopperCash

А як щодо багатопроцесорних систем?
Технофіл

65

По-справжньому! У моїх тестах (C ++) я розділив це на декілька категорій по ряду використовуваного шаблону одночасності:

  1. Тестові одиниці для класів, які працюють в одній нитці і не обізнані в потоці - легко, протестуйте як завжди.

  2. Тестові одиниці об'єктів Monitor (ті, які виконують синхронізовані методи в потоці управління абонентів), які виявляють синхронізований загальнодоступний API - створюють декілька макетних потоків, що здійснюють API. Побудуйте сценарії, що реалізують внутрішні умови пасивного об’єкта. Включіть один більш тривалий тест, який в основному вибиває хек з нього з декількох потоків протягом тривалого періоду часу. Я це не науково знаю, але це створює впевненість.

  3. Тестові одиниці для активних об'єктів (тих, які інкапсулюють власну нитку або нитки управління) - аналогічно №2 вище з варіаціями залежно від дизайну класу. Загальнодоступний API може блокувати або не блокувати, абоненти можуть отримувати ф'ючерси, дані можуть надходити в черги або потребувати видалення. Тут можливе багато комбінацій; білий ящик далеко. Ще потрібно декілька макетних потоків для здійснення дзвінків до тестуваного об'єкта.

Як осторонь:

У внутрішніх тренінгах для розробників, які я роблю, я викладаю « Стовпи конкурентності» і ці два шаблони як основну основу для роздумів і розв'язування проблем з одночасністю. Там, очевидно, є більш просунуті концепції, але я виявив, що цей набір основ допомагає інженерам не залишати супу. Це також призводить до коду, який є більш перевіреним, як описано вище.


51

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

Написання перевіреного багатопотокового коду

Перше, що потрібно зробити - це відокремити свій код обробки обробної нитки від усього коду, який здійснює фактичну обробку даних. Таким чином, обробка даних може бути перевірена як однопотоковий код, і єдине, що багатопотоковий код робить - це координувати нитки.

Друге, що слід пам’ятати, це те, що помилки в багатопотоковому коді є імовірнісними; помилки, які проявляються найменш часто - це клопи, які проникнуть у виробництво, їх буде важко відтворити навіть у виробництві, і, таким чином, викличуть найбільші проблеми. З цієї причини стандартний підхід до кодування швидкого запису коду та його налагодження, поки він не працює, є поганою ідеєю для багатопотокового коду; це призведе до коду, де виправлені легкі помилки, а небезпечні помилки все ще є.

Натомість, коли ви пишете багатопотоковий код, ви повинні писати код із таким ставленням, що ви збираєтесь у першу чергу уникати написання помилок. Якщо ви правильно видалили код обробки даних, код обробки потоків повинен бути досить малим - бажано, декількома рядками, в гіршому випадку - декількома десятками рядків - щоб у вас був шанс написати це, не написавши помилку, і, звичайно, не написавши багато помилок , якщо ви розумієте різьблення, не витрачайте час і будьте обережні.

Написання одиничних тестів для багатопотокового коду

Після того, як багатопотоковий код буде написаний максимально ретельно, все одно варто написати тести для цього коду. Основна мета тестів - це не стільки тестування на сильно залежні від строкових помилок станів гонки - неможливо повторно перевірити такі умови перегонів - а скоріше перевірити, що ваша стратегія блокування для запобігання таких помилок дозволяє взаємодіяти декілька потоків за призначенням .

Щоб правильно перевірити правильну поведінку блокування, тест повинен запустити кілька потоків. Щоб зробити тест повторюваним, ми хочемо, щоб взаємодії між потоками відбувалися в передбачуваному порядку. Ми не хочемо зовнішньо синхронізувати потоки в тесті, тому що це маскує помилки, які можуть статися у виробництві, де потоки не синхронізовані зовні. Це залишає використання затримок синхронізації для синхронізації потоків, що є технікою, яку я успішно використовував, коли мені доводилося писати тести багатопотокового коду.

Якщо затримки занадто короткі, то тест стає крихким, оскільки незначні різниці в часі - скажімо, між різними машинами, на яких можна проводити тести - можуть спричинити вимкнення часу і тест. Те, що я зазвичай робив, - це починати із затримок, які спричиняють збої тесту, збільшуйте затримки, щоб тест надійно проходив на моїй розроблювальній машині, а потім подвоюйте затримки поза цим, тому тест має хороші шанси пройти на інших машинах. Це означає, що тест займе макроскопічну кількість часу, хоча, на мій досвід, ретельна тестова конструкція може обмежити цей час не більше десятка секунд. Оскільки у вашій програмі не повинно бути дуже багато місць, які вимагають код координації потоків, це повинно бути прийнятним для вашого тестового набору.

Нарешті, слідкуйте за кількістю помилок, виявлених вашим тестом. Якщо ваш тест має 80% кодового покриття, можна очікувати, що він зачепить близько 80% ваших помилок. Якщо ваш тест добре розроблений, але не виявлено помилок, є обґрунтований шанс, що у вас немає додаткових помилок, які з’являться лише у виробництві. Якщо тест виявить одну або дві помилки, можливо, вам все-таки пощастить. Крім цього, і ви можете розглянути ретельний огляд або навіть повне перезапис вашого коду обробки потоків, оскільки, ймовірно, код все ще містить приховані помилки, які буде дуже важко знайти, поки код не з'явиться у виробництві, і дуже важко виправити тоді.


3
Тестування може виявити лише наявність помилок, а не їх відсутність. Оригінальне запитання задає проблему з двома потоками, у цьому випадку можливе вичерпне тестування, але часто це не так. Для будь-якого, що не відповідає найпростішим сценаріям, можливо, вам доведеться кусати кулю і використовувати формальні методи - але не пропускайте одиничні тести! Написати правильний багатопотоковий код важко в першу чергу, але не менш важкою проблемою є захист у майбутньому проти регресії.
Пол Вільямс

4
Дивовижний підсумок одного з найменш зрозумілих способів. Ваша відповідь - напад на справжню сегрегацію, яку ppl взагалі не помічає.
праш

1
Десяток секунд - це досить тривалий час, навіть якщо у вас є лише кілька сотень тестів такої довжини ...
Toby Speight

1
@TobySpeight Тести довгі порівняно зі звичайними одиничними тестами. Я виявив, що півдюжини тестів є більш ніж достатнім, якщо потіковий код належним чином спроектований максимально просто, хоча - потребуючи декількох сотень багатопотокових тестів, майже напевно вказуватимуть на надто складну схему накладання різьби.
Warren Dew

2
Це хороший аргумент для того, щоб зберігати логіку потоку якнайдалі від функціональності (я знаю, набагато простіше сказати, ніж зробити). І, якщо можливо, розбиття тестового набору на набори "на кожну зміну" та "попереднє фіксування" (тому ваші хвилинні хвилини на тести не надто впливають).
Toby Speight

22

У мене також були серйозні проблеми з тестуванням багатопотокового коду. Тоді я знайшов справді круте рішення у "xUnit Test Patterns" Джерара Медзароса. Описаний ним шаблон називається Humble object .

В основному це описує, як можна витягнути логіку в окремий, легкий для тестування компонент, який відокремлений від свого оточення. Після того, як ви перевірили цю логіку, ви можете перевірити складну поведінку (багатопотоковість, асинхронне виконання тощо).


20

Навколо є кілька інструментів, які досить непогані. Ось короткий опис Java.

Деякі хороші інструменти статичного аналізу включають FindBugs (дає корисні підказки), JLint , Java Pathfinder (JPF & JPF2) та Bogor .

MultithreadedTC - це непоганий інструмент динамічного аналізу (інтегрований у JUnit), де вам потрібно налаштувати власні тестові справи.

Цікавий ConTest від IBM Research. Він інструментує ваш код, вставляючи всі види поведінки, що змінюють потоки (наприклад, сон і вихід), щоб спробувати розкрити помилки випадковим чином.

SPIN - це дійсно класний інструмент для моделювання ваших Java (та інших) компонентів, але вам потрібно мати корисну основу. Важко використовувати як є, але надзвичайно потужний, якщо ви знаєте, як ним користуватися. Досить мало інструментів використовують SPIN під кришкою.

MultithreadedTC - це, мабуть, самий основний потік, але деякі з перерахованих вище засобів статичного аналізу, безумовно, варто переглянути.


16

Очікування також може бути корисним, щоб допомогти вам написати детерміновані одиничні тести. Це дозволяє зачекати, поки деякий стан десь у вашій системі не оновиться. Наприклад:

await().untilCall( to(myService).myMethod(), greaterThan(3) );

або

await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));

Він також має підтримку Scala та Groovy.

await until { something() > 4 } // Scala example

1
Чекання геніальне - саме те, що я шукав!
Forge_7

14

Інший спосіб (вигляд) тестування потокового коду та дуже складних систем загалом - через Fuzz Testing . Це не чудово, і він не знайде все, але він, ймовірно, буде корисним і його просто зробити.

Цитата:

Fuzz тестування або fzzing - це техніка тестування програмного забезпечення, яка надає випадкові дані ("fuzz") на входи програми. Якщо програма виходить з ладу (наприклад, при збої або через відмову від вбудованого кодового твердження), дефекти можна зазначити. Великою перевагою нечіткого тестування є те, що тестова конструкція надзвичайно проста та без попередніх уявлень про поведінку системи.

...

Тест Fuzz часто застосовується у великих проектах з розробки програмного забезпечення, які використовують тестування чорної скриньки. Ці проекти зазвичай мають бюджет на розробку інструментів тестування, а нечітке тестування - одна з методик, яка пропонує високий коефіцієнт користі та витрат.

...

Однак тестова перевірка не є заміною вичерпного тестування або формальних методів: вона може надати лише випадкову вибірку поведінки системи, і в багатьох випадках проходження нечіткого тесту може лише продемонструвати, що частина програмного забезпечення обробляє винятки без збоїв, а не поводиться правильно. Таким чином, нечітке тестування може розглядатися лише як інструмент пошуку помилок, а не як гарантія якості.


13

Я багато цього зробив, і так, це відстійно.

Деякі поради:

  • GroboUtils для запуску декількох тестових потоків
  • alphaWorks ConTest для інструментальних класів, щоб викликати переплетення, що відрізняються між ітераціями
  • Створіть throwableполе та позначте його tearDown(див. Лістинг 1). Якщо ви вловили поганий виняток в іншій нитці, просто призначте її за передачу.
  • Я створив клас утилітів у Лістингу 2 і вважав його безцінним, особливо waitForVerify та waitForCondition, що значно підвищить ефективність ваших тестів.
  • Корисно використати AtomicBooleanсвої тести. Це безпечно для потоків, і вам часто знадобиться остаточний тип посилання для зберігання значень класів зворотного дзвінка тощо. Дивіться приклад у Лістингу 3.
  • Переконайтеся, що завжди дайте термін очікування на тест (наприклад, @Test(timeout=60*1000)), оскільки тести на паралельність іноді можуть навішуватися назавжди, коли вони порушені.

Лістинг 1:

@After
public void tearDown() {
    if ( throwable != null )
        throw throwable;
}

Лістинг 2:

import static org.junit.Assert.fail;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Random;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;
import org.easymock.EasyMock;
import org.easymock.classextension.internal.ClassExtensionHelper;
import static org.easymock.classextension.EasyMock.*;

import ca.digitalrapids.io.DRFileUtils;

/**
 * Various utilities for testing
 */
public abstract class DRTestUtils
{
    static private Random random = new Random();

/** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with
 * default max wait and check period values.
 */
static public void waitForCondition(Predicate predicate, String errorMessage) 
    throws Throwable
{
    waitForCondition(null, null, predicate, errorMessage);
}

/** Blocks until a condition is true, throwing an {@link AssertionError} if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param errorMessage message use in the {@link AssertionError}
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, String errorMessage) throws Throwable 
{
    waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() {
        public void execute(Object errorMessage)
        {
            fail((String)errorMessage);
        }
    }, errorMessage);
}

/** Blocks until a condition is true, running a closure if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param closure closure to run
 * @param argument argument for closure
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, Closure closure, Object argument) throws Throwable 
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    if ( checkPeriod_ms == null )
        checkPeriod_ms = 100;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    while ( !predicate.evaluate(null) ) {
        Thread.sleep(checkPeriod_ms);
        if ( stopWatch.getTime() > maxWait_ms ) {
            closure.execute(argument);
        }
    }
}

/** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code>
 * for {@code maxWait_ms}
 */
static public void waitForVerify(Object easyMockProxy)
    throws Throwable
{
    waitForVerify(null, easyMockProxy);
}

/** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a
 * max wait time has elapsed.
 * @param maxWait_ms Max wait time. <code>null</code> defaults to 30s.
 * @param easyMockProxy Proxy to call verify on
 * @throws Throwable
 */
static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy)
    throws Throwable
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for(;;) {
        try
        {
            verify(easyMockProxy);
            break;
        }
        catch (AssertionError e)
        {
            if ( stopWatch.getTime() > maxWait_ms )
                throw e;
            Thread.sleep(100);
        }
    }
}

/** Returns a path to a directory in the temp dir with the name of the given
 * class. This is useful for temporary test files.
 * @param aClass test class for which to create dir
 * @return the path
 */
static public String getTestDirPathForTestClass(Object object) 
{

    String filename = object instanceof Class ? 
        ((Class)object).getName() :
        object.getClass().getName();
    return DRFileUtils.getTempDir() + File.separator + 
        filename;
}

static public byte[] createRandomByteArray(int bytesLength)
{
    byte[] sourceBytes = new byte[bytesLength];
    random.nextBytes(sourceBytes);
    return sourceBytes;
}

/** Returns <code>true</code> if the given object is an EasyMock mock object 
 */
static public boolean isEasyMockMock(Object object) {
    try {
        InvocationHandler invocationHandler = Proxy
                .getInvocationHandler(object);
        return invocationHandler.getClass().getName().contains("easymock");
    } catch (IllegalArgumentException e) {
        return false;
    }
}
}

Лістинг 3:

@Test
public void testSomething() {
    final AtomicBoolean called = new AtomicBoolean(false);
    subject.setCallback(new SomeCallback() {
        public void callback(Object arg) {
            // check arg here
            called.set(true);
        }
    });
    subject.run();
    assertTrue(called.get());
}

2
Час очікування - хороша ідея, але якщо випробування закінчиться, будь-які пізніші результати цього запуску підозрілі. Тест із вичерпаним часом може все ще мати деякі теми, які можуть зіпсувати вас.
Дон Кіркбі

12

Тестування МТ-коду на правильність - це, як уже було сказано, досить важка проблема. Зрештою, це зводиться до того, щоб у вашому коді не було неправильно синхронізованих перегонів даних. Проблема в цьому полягає в тому, що існує нескінченно багато можливостей виконання потоків (переплетень), над якими ви не маєте великого контролю ( хоч обов'язково прочитайте цю статтю). У простих сценаріях можна фактично довести коректність аргументацією, але зазвичай це не так. Особливо, якщо ви хочете уникнути / звести до мінімуму синхронізацію і не ходити на найбільш очевидний / найпростіший варіант синхронізації.

Підхід, який я дотримуюся, полягає в написанні дуже одночасного тестового коду для того, щоб зробити потенційно невиявленими перегони даних. А потім я запускаю ті тести на деякий час :) Я одного разу натрапив на розмову, де якийсь комп'ютерний вчений, де демонструє такий інструмент, який робить це (випадковим чином розробляє тест із специфікацій, а потім виконує їх дико, паралельно, перевіряючи визначені інваріанти зламати).

До речі, я думаю, що цей аспект тестування коду МТ тут не згадувався: визначте інваріанти коду, які ви можете перевірити випадковим чином. На жаль, пошук цих інваріантів теж досить важка проблема. Крім того, вони можуть не затримуватися весь час під час виконання, тому вам доведеться знайти / примусово виконувати точки виконання, де ви можете очікувати їх правдивості. Довести виконання коду до такого стану також є важкою проблемою (і сама по собі може виникнути проблеми з одночасністю. Так, це чорт важко!

Деякі цікаві посилання для читання:


Автор посилається на рандомізацію при тестуванні. Це може бути QuickCheck , який переноситься на багато мов. Ви можете дивитись розмови про таке тестування для паралельної системи тут
Макс

6

Піт Гудліфф має серію про тестування одиничного коду.

Це важко. Я беру простіший вихід і намагаюся утримувати коди для нарізання тексту реальним тестом. Піт згадує, що те, що я роблю, це неправильно, але я або отримав право на розлуку, або мені просто пощастило.


6
Я прочитав дві опубліковані досі статті, і не вважав їх дуже корисними. Він просто говорить про труднощі, не даючи конкретних порад. Можливо, майбутні статті покращаться.
Дон Кіркбі

6

Для Java ознайомтеся з главою 12 JCIP . Є кілька конкретних прикладів написання детермінованих, багатопотокових одиничних тестів, щоб принаймні перевірити правильність та інваріанти паралельного коду.

"Доведення" безпеки потоку за допомогою модульних тестів набагато складніше. Я вважаю, що цьому краще допомагати тестування автоматизованої інтеграції на різних платформах / конфігураціях.


6

Мені подобається написати два або більше методів тестування для виконання на паралельних потоках, і кожен з них здійснює виклики в тестований об'єкт. Я використовував Sleep () дзвінки для координації порядку дзвінків з різних потоків, але це не дуже надійно. Це також набагато повільніше, оскільки вам доведеться спати досить довго, щоб час працював зазвичай.

Я знайшов багатопотокову бібліотеку Java Java з тієї самої групи, яка написала FindBugs. Це дозволяє вам вказувати порядок подій без використання режиму сну (), і це надійно. Я ще цього не пробував.

Найбільшим обмеженням цього підходу є те, що він дозволяє лише протестувати сценарії, на які ви підозрюєте, що спричинить проблеми. Як говорили інші, вам дійсно потрібно виділити свій багатопотоковий код на невелику кількість простих класів, щоб мати надію на ретельне тестування їх.

Після того, як ви ретельно перевірили сценарії, які, на вашу думку, спричинить неполадки, ненауковий тест, який на деякий час кидає купу одночасних запитів, - це хороший спосіб шукати несподівані проблеми.

Оновлення: я трохи пограв з багатопотоковою бібліотекою Java Java, і це добре працює. Я також переніс деякі його функції до .NET версії, яку я називаю TickingTest .


5

Я обробляю одиничні випробування різьбових компонентів так само, як я обробляю будь-яке тестування блоку, тобто з інверсією систем управління та ізоляції. Я розвиваюсь на .Net-арені і поза коробкою різьблення (серед іншого) дуже важко (я б сказав, майже неможливо) повністю ізолювати.

Тому я написав обгортки, які виглядають приблизно так (спрощено):

public interface IThread
{
    void Start();
    ...
}

public class ThreadWrapper : IThread
{
    private readonly Thread _thread;

    public ThreadWrapper(ThreadStart threadStart)
    {
        _thread = new Thread(threadStart);
    }

    public Start()
    {
        _thread.Start();
    }
}

public interface IThreadingManager
{
    IThread CreateThread(ThreadStart threadStart);
}

public class ThreadingManager : IThreadingManager
{
    public IThread CreateThread(ThreadStart threadStart)
    {
         return new ThreadWrapper(threadStart)
    }
}

Звідти я можу легко ввести IThreadingManager в свої компоненти і використовувати мою структуру ізоляції, щоб вибрати те, що потік веде себе так, як я очікував під час тесту.

Це поки що для мене спрацювало чудово, і я використовую той самий підхід для пулу ниток, речей у System.El Environment, Sleep тощо тощо.


5

Подивіться на мою відповідь на

Розробка тестового класу для користувацького бар'єру

Він упереджений до Java, але має розумний підсумок варіантів.

Якщо підсумовуватись (IMO), це не використання якоїсь фантазійної рамки, яка забезпечить правильність, а те, як ви вирішите створити ваш багатопотоковий код. Розділення проблем (паралельності та функціональності) проходить величезний шлях до підвищення довіри.Зростання об’єктно-орієнтованого програмного забезпечення, керованого тестами, пояснює деякі варіанти краще, ніж я можу.

Статичний аналіз та формальні методи (див., Конкурс: Державні моделі та програми Java ) - це варіант, але я виявив, що вони мають обмежене використання в комерційній розробці.

Не забувайте, що будь-які тести навантаження / замочування рідко гарантовано висвітлюють проблеми.

Удачі!


Ви також повинні згадати tempus-fugitтут свою бібліотеку, яка helps write and test concurrent code;)
Ідолон

4

Нещодавно я відкрив (для Java) інструмент під назвою Threadsafe. Це статичний інструмент аналізу, схожий на Findbugs, але спеціально для виявлення проблем із багатопотоковою резьбою. Це не є заміною для тестування, але я можу рекомендувати його як частину написання надійної багатопотокової Java.

Він навіть підхоплює деякі дуже тонкі потенційні проблеми навколо речей, таких як підключення класу, доступ до небезпечних об'єктів за допомогою одночасних класів та виявлення відсутніх мінливих модифікаторів при використанні подвійної перевіреної парадигми блокування.

Якщо ви пишете багатопотокову Java, дайте їй постріл.


3

Наступна стаття пропонує 2 рішення. Обгортання семафору (CountDownLatch) і додає функціональність, як екстерналізація даних із внутрішньої нитки. Ще одним способом досягнення цієї мети є використання потокового басейну (див. Цікаві місця).

Спринклер - Розширений об'єкт синхронізації


3
Поясніть, будь ласка, підходи, зовнішні посилання в майбутньому можуть бути мертвими.
Uooo

2

Більшу частину минулого тижня я провела в університетській бібліотеці, вивчаючи налагодження паралельного коду. Основна проблема - паралельний код недетермінований. Зазвичай академічна налагодження потрапила в один з трьох таборів тут:

  1. Подія-слід / повтор. Для цього потрібен моніторинг подій, а потім перегляд подій, які були надіслані. У рамках UT це передбачає надсилання подій вручну в рамках тесту, а потім проведення посмертних оглядів.
  2. Сценарій. Тут ви взаємодієте з запущеним кодом з набором тригерів. "On x> foo, baz ()". Це може бути інтерпретоване в рамку UT, коли у вас є система часу запуску заданого тесту за певної умови.
  3. Інтерактивний. Це, очевидно, не спрацює в автоматичному тестуванні. ;)

Тепер, як зауважили вище коментатори, ви можете спроектувати свою одночасну систему в більш детермінований стан. Однак якщо ви цього не зробите належним чином, ви знову повернетесь до проектування послідовної системи.

Моя пропозиція полягає в тому, щоб зосередити увагу на тому, щоб мати дуже суворий протокол дизайну щодо того, що стає потоковим, а що не має. Якщо ви обмежуєте свій інтерфейс, щоб між елементами були мінімальні залежності, це набагато простіше.

Удачі та продовжуйте працювати над проблемою.


2

У мене було невдале завдання випробувати потоковий код, і це, безумовно, найскладніші тести, які я коли-небудь писав.

Під час написання тестів я використовував поєднання делегатів та подій. По суті, це все про використання PropertyNotifyChangedподій з тим WaitCallbackчи іншимConditionalWaiter опитуванням.

Я не впевнений, чи це був найкращий підхід, але це спрацювало для мене.


1

Припускаючи, що під "багатопотоковим" кодом малося на увазі щось таке

  • державний і змінний
  • І доступ / модифікований кількома потоками одночасно

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

Оскільки цей звір рідкісний, перш за все нам потрібно переконатися в тому, що є всі вагомі виправдання для його написання.

Крок 1. Розгляньте зміну стану в одному контексті синхронізації.

Сьогодні легко написати сумісний одночасно і асинхронний код, у якому IO або інші повільні операції завантажуються на другий фон, але загальний стан оновлюється та запитується в одному контексті синхронізації. наприклад, завдання асинхронізації / очікування та Rx в .NET тощо. - всі вони перевіряються дизайном, "справжні" Завдання та планувальники можуть бути замінені, щоб зробити тестування детермінованим (однак це поза сферою питання).

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

Крок 2. Якщо маніпулювання спільним станом у єдиному контексті синхронізації абсолютно неможливо.

Переконайтесь, що колесо не винаходиться / напевно немає стандартної альтернативи, яка може бути адаптована до роботи. Мабуть, ймовірно, що код дуже згуртований і міститься в одній одиниці, наприклад, з хорошим шансом, це особливий випадок якоїсь стандартної структури даних, безпечної для потоків, як хеш-карта чи колекція чи що завгодно.

Примітка: якщо код великий / охоплює декілька класів І потребує багатопотокового маніпулювання станом, то є дуже високий шанс, що дизайн не гарний, перегляньте крок 1

Крок 3. Якщо цього кроку досягнуто, нам потрібно перевірити власний користувальницький клас / метод / одиницю, безпечний для потоків .

Я буду мертвим чесним: мені ніколи не доводилося писати належних тестів для такого коду. Більшу частину часу я відходжу на кроці 1, іноді на кроці 2. Востаннє мені доводилося писати спеціальний код, захищений від потоку, було стільки років тому, що це було до того, як я прийняв тестування одиниці / напевно, мені б не довелося його писати з поточними знаннями все одно.

Якби мені справді довелося перевірити такий код ( нарешті, фактична відповідь ), то я б спробував пару речей нижче

  1. Недетерміновані стрес-тести. наприклад, запустіть 100 ниток одночасно і перевірте, чи відповідає кінцевий результат. Це більш типово для тестування більш високого рівня / інтеграції для декількох сценаріїв користувачів, але також може використовуватися на рівні одиниці.

  2. Розкрийте кілька тестових «гачків», де тест може ввести якийсь код, щоб зробити детерміновані сценарії, коли одна нитка повинна виконувати операцію перед іншою. Як некрасиво це, я нічого кращого не можу придумати.

  3. Затримка на основі тестування, щоб зробити потоки запущеними та виконувати операції в певному порядку. Строго кажучи, такі тести теж не є детермінованими (є ймовірність заморозити / зупинити систему колекції GC, що може спотворити інакше організовані затримки), також це некрасиво, але дозволяє уникнути гаків.


0

Для коду J2E я використовував SilkPerformer, LoadRunner та JMeter для тестування паралельності потоків. Всі вони роблять те саме. В основному вони дають вам порівняно простий інтерфейс для адміністрування їх версії проксі-сервера, необхідний для аналізу потоку даних TCP / IP та моделювання декількох користувачів, що роблять одночасні запити на ваш сервер додатків. Проксі-сервер може надати вам можливість робити такі дії, як аналізувати зроблені запити, представляючи всю сторінку та URL-адресу, надіслану на сервер, а також відповідь від сервера після обробки запиту.

Ви можете знайти деякі помилки в незахищеному режимі http, де ви зможете хоча б проаналізувати дані форми, що надсилаються, та систематично змінювати дані для кожного користувача. Але справжні тести - це коли ви працюєте в https (Se secure Socket Layers). Тоді вам також доведеться боротися із систематичною зміною даних сеансу та файлів cookie, що може бути дещо складніше.

Найкраща помилка, яку я коли-небудь знаходив, під час тестування на одночасність, виявив, коли розробник покладався на збирання сміття Java, щоб закрити запит на з'єднання, встановлений при вході в систему, до сервера LDAP під час входу. Це призвело до того, що користувачі піддаються впливу до сеансів інших користувачів і дуже заплутаних результатів, коли намагаються проаналізувати, що сталося, коли сервер був поставлений на коліна, ледве в змозі виконати одну транзакцію кожні кілька секунд.

Врешті-решт, вам чи комусь, мабуть, доведеться придуматись та проаналізувати код на помилки, як той, про який я щойно згадував. І відкрита дискусія в різних відділах, як та, що відбулася, коли ми розгорнули описану вище проблему, є найбільш корисною. Але ці інструменти - найкраще рішення для тестування багатопотокового коду. JMeter є відкритим кодом. SilkPerformer та LoadRunner є власником. Якщо ви дійсно хочете знати, чи безпечний ваш додаток, саме так роблять це великі хлопці. Я робив це для дуже великих компаній професійно, тому не здогадуюсь. Я кажу з особистого досвіду.

Слово обережності: потрібен певний час, щоб зрозуміти ці інструменти. Це не буде питанням просто встановити програмне забезпечення та запустити графічний інтерфейс, якщо ви вже не зазнали багатопотокового програмування. Я намагався визначити 3 найважливіші категорії областей, які слід зрозуміти (форми, дані сеансу та файли cookie), сподіваючись, що принаймні почати з розуміння цих тем допоможе вам зосередитись на швидких результатах, а не на тому, щоб прочитати вся документація.


0

Паралельність - це складна взаємодія між моделлю пам'яті, обладнанням, кешами та нашим кодом. Що стосується Java, принаймні такі тести частково розглядаються jcstress . Як відомо, творці цієї бібліотеки є авторами багатьох функцій одночасності JVM, GC та Java.

Але навіть цій бібліотеці потрібні хороші знання специфікації Java Memory Model, щоб ми точно знали, що тестуємо. Але я думаю, що у центрі уваги цих зусиль - це міркобенчмарки. Не величезні бізнес-додатки.


0

Є стаття на цю тему, використовуючи Rust як мову в прикладі коду:

https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

Підводячи підсумок, хитрість полягає в тому, щоб записати свою одночасну логіку, щоб вона була надійною для недетермінізму, пов'язаного з декількома потоками виконання, використовуючи такі інструменти, як канали та споживачі.

Тоді, якщо саме так ви структурували свої "компоненти", найпростіший спосіб протестувати їх, використовуючи канали для надсилання їм повідомлень, а потім блокуйте на інших каналах, щоб стверджувати, що компонент надсилає певні очікувані повідомлення.

Пов’язана стаття повністю написана за допомогою тестування одиниць.


-1

Якщо ви випробовуєте просту нову тему (розбіжна) .run () Ви можете знущатися з теми, щоб послідовно запускати виконуваний

Наприклад, якщо код тестованого об'єкта викликає такий новий потік

Class TestedClass {
    public void doAsychOp() {
       new Thread(new myRunnable()).start();
    }
}

Тоді може допомогти глузування нових тем і послідовний запуск аргументу, який можна виконати

@Mock
private Thread threadMock;

@Test
public void myTest() throws Exception {
    PowerMockito.mockStatic(Thread.class);
    //when new thread is created execute runnable immediately 
    PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
        @Override
        public Thread answer(InvocationOnMock invocation) throws Throwable {
            // immediately run the runnable
            Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
            if(runnable != null) {
                runnable.run();
            }
            return threadMock;//return a mock so Thread.start() will do nothing         
        }
    }); 
    TestedClass testcls = new TestedClass()
    testcls.doAsychOp(); //will invoke myRunnable.run in current thread
    //.... check expected 
}

-3

(якщо можливо) не використовуйте теми, не використовуйте акторів / активних об'єктів. Легкий для тестування.


2
@OMTheEternity можливо, але все ж найкраща відповідь imo.
Кріп

-5

Ви можете використовувати EasyMock.makeThreadSafe, щоб зробити тестовий примірник безпечним


Це зовсім не можливий спосіб тестування багатопотокового коду. Проблема полягає не в тому, що тестовий код працює багатопотоково, а в тому, що ви перевіряєте код, який зазвичай працює багатопотоково. І ви не можете все синхронізувати далеко, оскільки тоді ви фактично більше не тестуєте на перегони даних.
bennidi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.