Java Time Machine

世界には、有名ではないが非常に便利な多くのクールで小さなライブラリがあります。 このアイデアは、タグ#javalifehackerの下でそのようなことをHabrに徐々に紹介することです 。 今日は、 time-testについてお話します。このテストでは、コミットは16件しかありませんが、十分な数があります。 ライブラリの作成者はNikita Kovalであり、これは元々 Devexpertsブログのために書かれた彼の記事の翻訳です。







長期にわたる作業に関連する機能の単体テストを作成するのは難しい場合があります。 場合によっては、時間を返すメソッドを使用して、その実装をテストコードに置き換えることができます。 しかし、実際のアプリケーションをテストするには、これだけでは不十分です。 そのようなソリューションが機能しない理由と、時間をテストするために本当に必要なものを見てみましょう。















世界の終わりまでの日数を数える最も簡単な方法は次のとおりです。







fun daysBeforeDoom() { return doomTime - System.currentTimeMillis()) / millisInDay }
      
      





ほとんどの場合、そのテストのために、既存のツール( 1、2 )を使用してSystem.currentTimeMillis()



すべての呼び出しを置き換えるASMまたはAspectJでコード変換を記述するだけで十分です(特殊な動作が必要な場合)。







しかし、このアプローチでは不十分な場合があります。 毎日目覚め、「世界の終わりまであと<N>日」というメッセージを表示する目覚まし時計を書いていると想像してください。







 while (true) { Thread.sleep(ONE_DAY) println("${daysBeforeDoom()} days left till the doomsday") }
      
      





しかし、このコードをテストする方法は? 毎日実際に実行され、正しいメッセージが表示されることを確認するにはどうすればよいですか? 上記の例の最も単純なアプローチを使用し、 System.currentTimeMillis()



を置き換えると、メッセージの正確さだけをチェックできます。 ただし、スケジュールの正確さをテストするには、1日中待つ必要があります。







したがって、追加のツールを使用せずにそのようなコードをテストすることはほとんど不可能です。 それではそれらを書きましょう!







そのため、時間を返すメソッドにはSystem.currentTimeMillis()



System.nanoTime()



2つがあります。 さらに、最大待機時間を指定できる同期メソッドがいくつかあります: Thread.sleep(..)



Object.wait(..)



およびLockSupport.park(..)









時間を管理するために、仮想時間を変更し、必要なスレッドを起動する、何らかの種類のincreaseTime(..)



メソッドを作成します。







これは、時間の経過とともに機能するすべてのメソッドをテスト実装に置き換えると実現できます。 これがどのように機能するかを見てみましょう。







テスト例:







 increaseTime(ONE_DAY) checkMessage()
      
      





このテストは、メッセージのチェックと、すぐには実行されない印刷操作の間の競合の可能性を作成することをおそらく既にお気づきでしょう。 もちろん、一時停止を追加することもできます。







 increaseTime(ONE_DAY) Thread.sleep(500 /*ms*/) checkMessage()
      
      





通常の生活では、このテストはほとんど常に機能しますが、メッセージが表示される前にcheckMessage()



checkMessage()



れないという本当の保証はありません。 これは、テストロジックの複雑さが増した結果、または単純に過負荷のサーバーでコードを実行したときに発生する可能性があります。 タイムアウトを増やしたいという要望があるかもしれませんが、この解決策はテストを遅くするだけであり、正確性の保証はまだありません。







代わりに、目覚めたすべてのスレッドがジョブを実行するまで待機する特別なメソッドが必要です。







理想的には、私はそのようなテストを書きたいです:







 increaseTime(ONE_DAY) waitUntilThreadsAreFrozen(1_000/*ms, timeout*/) checkMessage()
      
      





したがって、時間依存メソッドの仮想化だけでなく、同時に実行するのが容易ではないwaitUntilThreadsAreFrozen



メソッドもサポートする必要があります。







NikitaはDevexpertsで働いている間、この問題を解決するtime-testというツールを作成しました。 仕組みを見てみましょう。







タイムテストはJavaエージェントとして記述されています。 これを使用するには、 -javaagent:timetest.jar



を追加して、クラスパスに配置する必要があります。 このツールはバイトコードを変換し、時間の経過とともに機能するすべてのメソッドをその実装の呼び出しに置き換えます。 優れたJavaエージェントを書くことは、しばしば困難な作業です。そのため、Nikitaはこれを簡単にするJAgentフレームワークを開発しました







テストを作成するときは、 TestTimeProvider



を有効にする必要があります。 System.currentTimeMillis()



Thread.sleep(..)



Object.wait(..)



LockSupport.park(..)



などを含むSystem.currentTimeMillis()



必要なすべてのメソッドを実装し、通常の実装をオーバーライドします。 ほとんどのテストでは、基礎となる実装で使用される時間を直接制御する必要はありません。 したがって、TestTimeProviderに接続するまで、ツールは上記のメソッドのデフォルトの実装を引き続き使用し、独自のコードでラップします。 TestTimeProvider



接続した後、メソッドTestTimeProvider.setTime(..)



TestTimeProvider.increaseTime(..)



およびTestTimeProvider.waitUntilThreadsAreFrozen(..)



を使用できるようになりTestTimeProvider.waitUntilThreadsAreFrozen(..)









TimeProvider.java









 long timeMillis(); long nanoTime(); void sleep(long millis) throws InterruptedException; void sleep(long millis, int nanos) throws InterruptedException; void waitOn(Object monitor, long millis) throws InterruptedException; void waitOn(Object monitor, long millis, int nanos) throws InterruptedException; void notifyAll(Object monitor); void notify(Object monitor); void park(boolean isAbsolute, long time); void unpark(Object thread);
      
      





上記で書いたように、 TestTimeProvider



実装の主な問題は、時間とwaitUntilThreadsAreFrozen(..)



waitUntilThreadsAreFrozen(..)



するための両方のメソッドの同時サポートwaitUntilThreadsAreFrozen(..)



。 したがって、変更が行われるたびに、必要なすべてのスレッドが最初に動作中としてマークされ、その後にのみ起動します。 同時に、 waitUntilThreadsAreFrozen(..)



は、すべてのスレッドが待機状態になるまで待機するため、どのスレッドも動作中としてマークされません。 このアプローチの一環として、スレッドはウェイクアップし、マークをリセットし、タスクを完了して待機状態に戻りwaitUntilThreadsAreFrozen(..)



そして、 waitUntilThreadsAreFrozen(..)



が動作したことを理解するだけwaitUntilThreadsAreFrozen(..)









TestTimeProvider



を使用したテストの外観:







 @Before public void setup() { // Use TestTimeProvider for this test TestTimeProvider.start(/* initial time could be passed here */); } @After public void reset() { // Reset time provider to default after the test execution TestTimeProvider.reset(); } @Test public void test() { runMyConcurrentApplication(); TestTimeProvider.increaseTime(60_000 /*ms*/); TestTimeProvider.waitUntilThreadsAreFrozen(1_000 /*ms*/); checkMyApplicationState(); }
      
      





時間の仮想化にはもう1つの困難があります。 JVM全体の時間を制御する必要がある場合、説明したアプローチはうまく機能します。 しかし、結局のところ、通常、テストライブラリ(JUnitなど)、ガベージコレクタスレッド、およびテストされたコードフラグメントに直接関連しないその他のものには触れないようにします。 したがって、テストコードで実行しているかどうか、およびこれに基づいて時間を仮想化する必要があるかどうかを判断することが不可欠です。 このため、タイムテストはテストされたコードの入力ポイントを知っている必要があります(通常、これらはテストクラスです)。 次に、タイムテストが開始され、新しいスレッドの起動の追跡と「独自の」スレッドのマーク付けが行われます。つまり、時間の仮想化が適用されます。 ただし、 ForkJoinPool



使用すると、テストコードから実行されないため問題が発生する可能性があり、time-testはそこで時間を仮想化する必要があることを理解できません。 ForkJoinPoolに似たデザインを使用するには、入力ポイントの定義を拡張する必要があります。







時間が経つにつれて機能をテストすることは、それほど簡単な作業ではないかもしれないことが、今明らかになったと思います。 タイムテストがあなたの人生を楽にしてくれることを願っています。GitHubでソースコード入手できます







著者について



Nikita Kovalは、Devexperts dxLab研究チームの研究エンジニアです。 さらに、彼はITMOのコンピューターテクノロジー学部の学生であり、マルチスレッドプログラミングのコースも教えています。 彼は主に、マルチスレッドアルゴリズム、プログラム検証、およびそれらの分析に興味があります。







広告の分。 Nikitaは、レポート「Towards a Fast Multithreaded Hash Table」で、マルチコアアーキテクチャの全機能を使用して高性能アルゴリズムを構築するための実用的なアプローチについて、 JBreak 2018カンファレンス(2週間以内に開催)に参加します。 会議にはディスカッションゾーンがあります。レポートの後、Nikitaと会い、さまざまな問題について議論することができます-マルチスレッドハッシュテーブルだけでなく、記事で説明されている時間の仮想化も。 チケットは公式ウェブサイトで購入できます



All Articles