マルチスレッドコードをテストするという悪夢は、地雷のように思えるので、これまでは避けてきました。スレッドに依存して実行が成功するコードのテストに人々がどのように取り組んでいるのか、または2つのスレッドが特定の方法で相互作用するときにのみ表示される種類の問題のテストにどのように取り組んでいるのかを尋ねたいのですが。
これは、今日のプログラマーにとって本当に重要な問題のようです。この1つの私見に関する知識をプールしておくと役に立ちます。
マルチスレッドコードをテストするという悪夢は、地雷のように思えるので、これまでは避けてきました。スレッドに依存して実行が成功するコードのテストに人々がどのように取り組んでいるのか、または2つのスレッドが特定の方法で相互作用するときにのみ表示される種類の問題のテストにどのように取り組んでいるのかを尋ねたいのですが。
これは、今日のプログラマーにとって本当に重要な問題のようです。この1つの私見に関する知識をプールしておくと役に立ちます。
回答:
見て、これを行う簡単な方法はありません。私は本質的にマルチスレッドのプロジェクトに取り組んでいます。イベントはオペレーティングシステムから送られてくるので、同時に処理する必要があります。
複雑なマルチスレッドアプリケーションコードのテストを処理する最も簡単な方法は、次のとおりです。テストが複雑すぎて、テストが間違っています。複数のスレッドが動作する単一のインスタンスがあり、これらのスレッドが互いに重なり合っている状況をテストできない場合は、デザインをやり直す必要があります。これと同じくらいシンプルで複雑です。
複数のインスタンスを同時に実行するスレッドを回避するマルチスレッド化をプログラミングする方法は多数あります。最も簡単なのは、すべてのオブジェクトを不変にすることです。もちろん、それは通常不可能です。したがって、スレッドが同じインスタンスと相互作用するデザイン内の場所を特定し、それらの場所の数を減らす必要があります。これを行うことにより、マルチスレッドが実際に発生するいくつかのクラスを分離し、システムのテストの全体的な複雑さを軽減します。
しかし、これを行っても、2つのスレッドが互いに踏み込むすべての状況をテストできるわけではないことを理解する必要があります。そのためには、同じテストで2つのスレッドを同時に実行し、特定の瞬間に実行する行を正確に制御する必要があります。あなたができる最善のことは、この状況をシミュレートすることです。しかし、これはテストのために特別にコーディングする必要があるかもしれません、そしてそれは真の解決へのせいぜい半分のステップです。
おそらく、スレッドの問題についてコードをテストする最良の方法は、コードの静的分析によるものです。スレッド化されたコードがスレッドセーフパターンの有限セットに従っていない場合は、問題が発生している可能性があります。VSのコード分析にはスレッド化に関するある程度の知識が含まれていると思いますが、おそらくそれほど多くはありません。
状況が現状のままである(そして、おそらくこれから良い時期になるだろう)ので、マルチスレッドアプリをテストする最善の方法は、スレッドコードの複雑さをできるだけ減らすことです。スレッドが相互作用する領域を最小限に抑え、可能な限りテストし、コード分析を使用して危険な領域を特定します。
この質問が投稿されたのは久しぶりですが、まだ回答がありません...
kleolb02の答えは良いものです。詳細を調べてみます。
C#コード用に練習する方法があります。単体テストの場合、マルチスレッドコードで最大の課題である再現可能なテストをプログラミングできる必要があります。働くテストハーネス、に非同期コードを強制的に向けて私の答えを目指しので、同期。
これはGerard Meszardosの著書「xUnit Test Patterns」からのアイデアであり、「Humble Object」(p。695 )と呼ばれています。コアロジックコードと、非同期コードのような匂いがするものを分離する必要があります。これにより、同期して動作するコアロジックのクラスが生成されます。
これにより、コアロジックコードを同期的にテストすることができます。コアロジックで実行する呼び出しのタイミングを完全に制御できるため、再現可能なテストを実行できます。これは、コアロジックと非同期ロジックを分離することで得られるメリットです。
このコアロジックは、コアロジックへの呼び出しを非同期的に受信し、これらの呼び出しをコアロジックに委任する別のクラスによってラップされる必要があります。量産コードは、そのクラスを介してコアロジックにのみアクセスします。このクラスは呼び出しを委譲するだけなので、あまりロジックがない非常に「ダム」なクラスです。したがって、この非同期ワーキングクラスのユニットテストを最小限に保つことができます。
その上(クラス間の相互作用のテスト)はコンポーネントテストです。この場合も、「謙虚なオブジェクト」パターンに固執すれば、タイミングを完全に制御できるはずです。
確かにタフなもの!私の(C ++)単体テストでは、これを、使用する同時実行パターンのラインに沿っていくつかのカテゴリに分類しました。
シングルスレッドで動作し、スレッドに対応していないクラスの単体テスト-通常どおり、簡単にテストできます。
同期されたパブリックAPIを公開するMonitorオブジェクト(呼び出し元の制御スレッドで同期されたメソッドを実行するオブジェクト)の単体テスト-APIを実行する複数のモックスレッドをインスタンス化します。パッシブオブジェクトの内部条件を実行するシナリオを作成します。基本的に、複数のスレッドから長期間にわたって問題を解決する、より長い実行中のテストを1つ含めます。これは私が知っている非科学的ですが、自信を構築します。
アクティブオブジェクト(独自のスレッドまたは制御のスレッドをカプセル化するオブジェクト)の単体テスト-上記の#2と同様に、クラスの設計に応じてバリエーションがあります。パブリックAPIはブロックまたは非ブロックである場合があり、呼び出し元はフューチャーを取得する場合があり、データがキューに到着するか、またはデキューする必要がある場合があります。ここでは多くの組み合わせが可能です。離れたホワイトボックス。テスト対象のオブジェクトを呼び出すには、複数のモックスレッドが必要です。
余談として:
私が行う内部開発者トレーニングでは、並行性の柱とこれら2つのパターンを、並行性の問題を考えて分解するための主要なフレームワークとして教えています。明らかにより高度な概念がありますが、この基本セットがエンジニアをスープから遠ざけるのに役立つことがわかりました。また、上記のように、よりユニットテスト可能なコードにつながります。
いくつかのプロジェクトのスレッド処理コードを書くときに、私はこの問題に近年何度か直面しました。他の回答のほとんどは代替案を提供していますが、テストに関する質問には実際には回答しないため、私は遅い回答を提供しています。私の答えは、マルチスレッドコードに代わるものがない場合に対処しています。完全を期すためにコード設計の問題を取り上げますが、単体テストについても説明します。
テスト可能なマルチスレッドコードの記述
最初に行うことは、実際のデータ処理を行うすべてのコードから本番スレッド処理コードを分離することです。このようにして、データ処理をシングルスレッドコードとしてテストできます。マルチスレッドコードで行うことは、スレッドを調整することだけです。
2番目に覚えておかなければならないのは、マルチスレッドコードのバグは確率論的であることです。発生頻度が最も低いバグは、本番環境に忍び込み、本番環境でさえ再現することが困難であるため、最大の問題を引き起こします。このため、コードをすばやく記述し、機能するまでデバッグするという標準的なコーディングアプローチは、マルチスレッドコードには適していません。簡単なバグが修正され、危険なバグがまだ残っているコードになります。
代わりに、マルチスレッドコードを作成するときは、最初からバグを作成しないようにする姿勢でコードを作成する必要があります。データ処理コードを適切に削除した場合、スレッド処理コードは十分に小さく(できれば数行、最低でも数十行)して、バグを書かずに、そして多くのバグを書かずに書くことができるようにします。 、スレッドを理解している場合は、時間をかけて注意してください。
マルチスレッドコードの単体テストの記述
マルチスレッドコードをできる限り慎重に作成した後でも、そのコードのテストを作成することには価値があります。テストの主な目的は、非常にタイミングに依存する競合状態のバグをテストすることではなく、そのような競合状態を繰り返しテストすることは不可能ですが、そのようなバグを防ぐためのロック戦略により、複数のスレッドが意図したとおりに相互作用できることをテストすることです。
正しいロック動作を適切にテストするには、テストで複数のスレッドを開始する必要があります。テストを再現可能にするために、スレッド間の相互作用が予測可能な順序で発生するようにします。テストでスレッドを外部同期させたくないのは、スレッドが外部同期されていない本番環境で発生する可能性のあるバグを隠すためです。これにより、スレッドの同期にタイミング遅延を使用することができます。これは、マルチスレッドコードのテストを作成する必要が生じたときに常に使用してきた手法です。
遅延が短すぎると、テストが壊れやすくなります。テストが実行される可能性のある異なるマシン間などのわずかなタイミングの違いにより、タイミングがずれてテストが失敗する可能性があるためです。私が通常行っていることは、テストの失敗の原因となる遅延から開始し、遅延を増やしてテストが開発マシンで確実にパスするようにし、その後、遅延を2倍にして、テストが他のマシンにパスする可能性を高めます。これは、テストが巨視的な時間を要することを意味しますが、私の経験では、注意深いテスト設計により、その時間を12秒以内に制限できます。アプリケーションでスレッド調整コードを必要とする場所はそれほど多くないはずなので、テストスイートではそれで十分です。
最後に、テストで検出されたバグの数を追跡します。テストのコードカバレッジが80%の場合、バグの約80%をキャッチすることが期待できます。テストが適切に設計されていてもバグが見つからない場合、本番環境でのみ発生する追加のバグがない可能性は十分にあります。テストで1つまたは2つのバグが検出された場合でも、幸運になる可能性があります。それを超えて、コードを慎重に確認するか、スレッド処理コードの完全な書き直しを検討することをお勧めします。コードには、コードが本稼働するまで見つけるのが非常に困難で、その後修正するのは難しい。
かなり良いツールがいくつかあります。Javaのいくつかの要約を以下に示します。
いくつかの優れた静的分析ツールには、FindBugs(いくつかの有用なヒントが表示されます)、JLint、Java Pathfinder(JPF&JPF2)、Bogorなどがあります。
MultithreadedTCは非常に優れた動的分析ツール(JUnitに統合)であり、独自のテストケースを設定する必要があります。
IBM ResearchのConTestは興味深いものです。あらゆる種類のスレッド変更動作(たとえば、スリープと利回り)を挿入してコードを計測し、バグをランダムに発見しようとします。
SPINは、Java(およびその他の)コンポーネントをモデル化するための本当にクールなツールですが、いくつかの有用なフレームワークが必要です。そのまま使うのは難しいですが、使い方がわかれば非常に強力です。かなりの数のツールが内部でSPINを使用しています。
MultithreadedTCはおそらく最も主流ですが、上記の静的分析ツールのいくつかは一見の価値があります。
待機時間は、確定的単体テストの作成にも役立ちます。システムのどこかの状態が更新されるまで待つことができます。例えば:
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つです。
...
ただし、ファズテストは、完全なテストや正式な方法の代わりにはなりません。システムの動作のランダムなサンプルしか提供できず、多くの場合、ファズテストに合格すると、ソフトウェアではなく、クラッシュせずに例外が処理されることを示す場合があります。正しく動作します。したがって、ファズテストは、品質を保証するものではなく、バグ検出ツールと見なすことができます。
私はこれをたくさんやった、そしてそれはうんざりだ。
いくつかのヒント:
throwable
フィールドを作成してチェックインしますtearDown
(リスト1を参照)。別のスレッドで不正な例外をキャッチした場合は、その例外をスロー可能オブジェクトに割り当ててください。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());
}
MTコードの正当性をテストすることは、すでに述べたように、かなり難しい問題です。結局のところ、コード内で誤って同期されたデータの競合がないことを確認することになります。これの問題は、スレッドの実行(インターリーブ)の可能性が無限にあり、それを制御できないことです(ただし、この記事を必ずお読みください)。単純なシナリオでは、推論によって実際に正しさを証明することが可能かもしれませんが、これは通常そうではありません。特に、同期を回避/最小化し、最も明白/最も簡単な同期オプションを選択しない場合。
私が従うアプローチは、潜在的に検出されないデータ競合が発生する可能性を高くするために、高度な並行テストコードを記述することです。そして、それらのテストをしばらく実行します:)私はかつて、ある種のコンピュータ科学者がこれを行うツールを見せびらかした話に偶然出会いました(仕様からテストをランダムに考案し、同時に乱暴に実行して、定義された不変条件をチェックします)壊れる)。
ちなみに、MTコードをテストするこの側面についてはここでは触れていません。ランダムにチェックできるコードの不変条件を特定します。残念ながら、それらの不変条件を見つけることも非常に難しい問題です。また、実行中は常に保持されない可能性があるため、それらが真であると期待できる実行ポイントを見つけて適用する必要があります。コードの実行をそのような状態にすることも難しい問題です(そしてそれ自体が同時実行性の問題を引き起こす可能性があります。
読むべきいくつかの興味深いリンク:
Pete Goodliffeが、スレッド化されたコードの単体テストに関するシリーズを公開しています。
それは難しい。私はより簡単な方法を取り、スレッド化コードを実際のテストから抽象化したままにします。ピートは私がそれをする方法は間違っていると述べていますが、私は分離を正しく行ったか、私は幸運でした。
並列スレッドで実行する2つ以上のテストメソッドを記述し、それぞれがテスト対象のオブジェクトを呼び出します。さまざまなスレッドからの呼び出しの順序を調整するためにSleep()呼び出しを使用してきましたが、それは本当に信頼できません。また、通常はタイミングが機能するのに十分な長さの睡眠をとる必要があるため、速度もかなり遅くなります。
FindBugsを書いたのと同じグループからマルチスレッドTC Javaライブラリを見つけました。Sleep()を使用せずにイベントの順序を指定できるため、信頼性が高くなります。まだ試していません。
このアプローチの最大の制限は、問題が発生する可能性があると疑われるシナリオのみをテストできることです。他の人が言ったように、マルチスレッド化されたコードを少数の単純なクラスに分離して、それらを徹底的にテストすることを期待する必要があります。
問題が発生すると予想されるシナリオを慎重にテストしたら、しばらくの間クラスで多数の同時要求をスローする非科学的なテストは、予期しない問題を探すための良い方法です。
更新:マルチスレッドのTC Javaライブラリを少し試してみましたが、うまく機能します。また、その機能の一部をTickingTestと呼ぶ.NETバージョンに移植しました。
スレッド化されたコンポーネントの単体テストは、あらゆる単体テストと同じように処理します。つまり、制御フレームワークと分離フレームワークを反転させます。私は.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.Environment、Sleepなどの同じアプローチを使用しています。
私の関連する答えを見てください
それはJavaに偏っていますが、オプションの合理的な要約があります。
要約すると(IMO)は、正確さを保証するいくつかの豪華なフレームワークを使用するのではなく、マルチスレッドコードの設計をどのように行うかです。懸念事項(同時実行性と機能性)を分割することは、信頼を高めるための大きな方法です。テストによって導かれる成長するオブジェクト指向ソフトウェアは、私ができるよりもいくつかのオプションを説明しています。
静的分析と正式なメソッド(「同時実行性:状態モデルとJavaプログラム」を参照)はオプションですが、商業開発での使用は限定的であることがわかりました。
ロード/ソークスタイルのテストでは、問題を強調することがほとんど保証されていないことを忘れないでください。
幸運を!
tempus-fugit
ここであなたのライブラリにも言及する必要がありますhelps write and test concurrent code
;)
次の記事は2つのソリューションを提案しています。セマフォをラップし(CountDownLatch)、内部スレッドからデータを外部化するなどの機能を追加します。この目的を達成するもう1つの方法は、スレッドプールを使用することです(「要点」を参照)。
先週のほとんどを大学の図書館で過ごし、並行コードのデバッグについて研究しました。中心的な問題は、並行コードが非決定的であることです。通常、アカデミックデバッグは次の3つのキャンプの1つに分類されます。
これで、上記のコメンテーターが気づいたように、並行システムをより確定的な状態に設計できます。ただし、これを適切に行わない場合は、シーケンシャルシステムの設計に戻ります。
私の提案は、何がスレッド化され、何がスレッド化されないかについて、非常に厳密な設計プロトコルに集中することです。要素間の依存関係が最小限になるようにインターフェースを制約すると、はるかに簡単になります。
頑張って、問題に取り組み続けてください。
「マルチスレッド」コードのもとでは、
言い換えれば、カスタムステートフルスレッドセーフクラス/メソッド/ユニットのテストについて話しているのです。
この獣はまれなので、まず最初に、それを書くためのすべての正当な言い訳があることを確認する必要があります。
手順1.同じ同期コンテキストで状態を変更することを検討してください。
今日では、IOなどの遅い操作がバックグラウンドにオフロードされているが、共有状態が1つの同期コンテキストで更新および照会される、構成可能な並行および非同期コードを簡単に作成できます。例:async / awaitタスクおよび.NETでのRxなど-これらはすべて設計によりテスト可能です。「実際の」タスクとスケジューラーを代わりに使用して、テストを確定的にすることができます(ただし、これは問題の範囲外です)。
非常に制約されているように聞こえるかもしれませんが、このアプローチは驚くほどうまく機能します。状態をスレッドセーフにする必要なく、このスタイルでアプリ全体を書くことができます(私はそうします)。
手順2.単一の同期コンテキストで共有状態を操作することが絶対に不可能である場合。
車輪が再発明されていないことを確認してください/仕事に適応できる標準的な代替手段がないことは間違いありません。コードは非常にまとまりがあり、1つのユニット内に含まれている可能性があります。たとえば、ハッシュマップやコレクションなどの標準的なスレッドセーフデータ構造の特殊なケースである可能性が高いです。
注:コードが大きい、または複数のクラスにまたがり、マルチスレッドの状態操作が必要な場合は、デザインが良くない可能性が非常に高いため、ステップ1を再検討してください
ステップ3.このステップに達した場合は、独自のカスタムステートフルスレッドセーフクラス/メソッド/ユニットをテストする必要があります。
私は正直に言うと、そのようなコードの適切なテストを書く必要はありませんでした。ほとんどの場合、ステップ1、時にはステップ2で離れます。最後にカスタムスレッドセーフコードを記述しなければならなかったのは、ユニットテストを採用する前であったため、おそらく記述する必要はなかったでしょう。とにかく現在の知識で。
私が本当にそのようなコードをテストしなければならなかった場合(最後に、実際の答え)、以下のことをいくつか試します
非決定論的ストレステスト。たとえば、100スレッドを同時に実行し、最終結果が一貫していることを確認します。これは、複数のユーザーのシナリオのより高いレベル/統合テストの場合により一般的ですが、ユニットレベルでも使用できます。
1つのスレッドが他のスレッドより先に操作を実行する必要がある決定論的なシナリオを作成するのに役立つコードを挿入できるテスト「フック」を公開します。醜いので、これ以上何も考えられません。
スレッドを特定の順序で実行および実行させるための遅延駆動テスト。厳密に言えば、このようなテストも非決定的です(システムのフリーズ/ストップザワールドGCコレクションの可能性があり、他の方法で調整された遅延をゆがめる可能性があります)。また、醜いですが、フックを回避できます。
J2Eコードでは、スレッドの同時実行性テストにSilkPerformer、LoadRunner、およびJMeterを使用しました。彼らはすべて同じことをします。基本的に、TCP / IPデータストリームを分析し、アプリサーバーに同時にリクエストを行う複数のユーザーをシミュレートするために必要な、プロキシサーバーのバージョンを管理するための比較的シンプルなインターフェースを提供します。プロキシサーバーは、リクエストを処理した後、サーバーに送信されたページ全体とURL、およびサーバーからの応答を提示することにより、リクエストの分析などの機能を提供します。
安全でないhttpモードでいくつかのバグを見つけることができます。このモードでは、送信されるフォームデータを少なくとも分析し、それを各ユーザーに対して体系的に変更できます。しかし、真のテストは、https(Secured Socket Layers)で実行した場合です。次に、セッションとCookieデータを体系的に変更する必要がありますが、これは少し複雑になる場合があります。
私がこれまでに見つけた最大のバグは、並行性のテスト中に、開発者がログイン時に確立されたLDAPサーバーへの接続要求をJavaガベージコレクションに依存してログインしていることを発見したときでした。これにより、ユーザーが公開されました。他のユーザーのセッションと非常に紛らわしい結果に対して、サーバーが停止したときに何が起こったかを分析しようとすると、数秒ごとに1つのトランザクションをやっと完了することができません。
結局のところ、あなたや誰かが、たった今述べたようなコードの失敗を見つけて、コードを分析する必要があるでしょう。また、上記の問題を明らかにしたときに発生したような、部門間のオープンディスカッションが最も役立ちます。しかし、これらのツールはマルチスレッドコードをテストするための最良のソリューションです。JMeterはオープンソースです。SilkPerformerとLoadRunnerは独自仕様です。アプリがスレッドセーフであるかどうかを本当に知りたいのであれば、それが大物が行う方法です。私はこれを大企業で専門的に行ったので、私は推測していません。私は個人的な経験から話しています。
注意:これらのツールを理解するには時間がかかります。マルチスレッドプログラミングにある程度触れたことがない限り、ソフトウェアをインストールしてGUIを起動するだけでは問題になりません。私は理解する必要がある領域の3つの重要なカテゴリ(フォーム、セッション、およびCookieデータ)を特定しようとしましたが、少なくともこれらのトピックを理解することから始めて、ドキュメント全体。
サンプルコードの言語としてRustを使用したトピックに関する記事があります。
https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a
要約すると、チャネルやcondvarsなどのツールを使用して、複数の実行スレッドに関連する非決定性に対して堅牢になるように、並行ロジックを作成することがトリックです。
次に、それが「コンポーネント」を構成した方法である場合、それらをテストする最も簡単な方法は、チャネルを使用してメッセージを送信し、他のチャネルをブロックして、コンポーネントが特定の予期されるメッセージを送信することをアサートすることです。
リンク先の記事は、単体テストを使用して完全に記述されています。
単純な新しいThread(runnable).run()をテストする場合は 、Threadをモックして実行可能ファイルを順次実行できます。
たとえば、テストされたオブジェクトのコードがこのような新しいスレッドを呼び出す場合
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
}
EasyMock.makeThreadSafeを使用して、テストインスタンスをスレッドセーフにすることができます。