40

サードパーティの Java API を使用して独自のアプリケーションからアクセスするバックエンド システムがあります。私は他のユーザーと一緒に通常のユーザーとしてシステムにアクセスできますが、私には神のような権限はありません。

したがって、テストを簡素化するために、実際のセッションを実行して API 呼び出しを記録し、それらを (できれば編集可能なコードとして) 保持したいと思います。これにより、記録セッションから対応する応答を返すだけの API 呼び出しで、後でドライ テストを実行できます。これは重要な部分です - 上記のバックエンド システムと対話する必要はありません。

したがって、アプリケーションにフォームに次の行が含まれている場合:

 Object b = callBackend(a);

引数 a を指定して bを返すフレームワークを最初にキャプチャしcallBackend()、後でドライランを実行するときに、「ねえ、この呼び出しが b を返す必要がある場合」と言います。a と b の値は同じになります (そうでない場合は、記録ステップを再実行します)。

API を提供するクラスをオーバーライドして、キャプチャするすべてのメソッド呼び出しがコードを通過するようにすることができます (つまり、制御外のクラスの動作を変更するためのバイト コード インストルメンテーションは必要ありません)。

これを行うには、どのフレームワークを調べる必要がありますか?


編集:賞金稼ぎは、私が探している動作を示す実際のコードを提供する必要があることに注意してください。

4

9 に答える 9

7

実際にプロキシパターンを利用することで、そのようなフレームワークやテンプレートを構築することができます。ここでは、動的プロキシ パターンを使用してそれを行う方法について説明します。アイデアは、

  1. API のレコーダおよびリプレーヤ プロキシをオンデマンドで取得するためのプロキシ マネージャを作成します
  2. 収集した情報を格納するラッパー クラスを記述し、同様のデータ構造から効率的に検索するためにそのラッパー クラスのメソッドを実装hashCodeします。equalsMap
  3. 最後に、レコーダ プロキシを使用して記録し、再生目的でリプレーヤ プロキシを使用します。

レコーダーの仕組み:

  1. 実際の API を呼び出す
  2. 呼び出し情報を収集します
  3. 期待される永続化コンテキストでデータを永続化します

リプレーヤーの仕組み:

  1. メソッド情報(メソッド名、パラメータ)を収集する
  2. 収集された情報が以前に記録された情報と一致する場合は、以前に収集された戻り値を返します。
  3. 返された値が一致しない場合は、収集した情報を永続化します (必要に応じて)。

では、実装を見てみましょう。API が次MyApiのような場合:

public interface MyApi {
    public String getMySpouse(String myName);
    public int getMyAge(String myName);
    ...
}

次に、 の呼び出しを記録して再生しますpublic String getMySpouse(String myName)。これを行うには、以下のような呼び出し情報を格納するクラスを使用できます。

    public class RecordedInformation {
       private String methodName;
       private Object[] args;
       private Object returnValue;

        public String getMethodName() {
            return methodName;
        }

        public void setMethodName(String methodName) {
            this.methodName = methodName;
        }

        public Object[] getArgs() {
            return args;
        }

        public void setArgs(Object[] args) {
            this.args = args;
        }

        public Object getReturnValue() {
            return returnType;
        }

        public void setReturnValue(Object returnValue) {
            this.returnValue = returnValue;
        }

        @Override
        public int hashCode() {
            return super.hashCode();  //change your implementation as you like!
        }

        @Override
        public boolean equals(Object obj) {
            return super.equals(obj);    //change your implementation as you like!
        }
    }

ここからが本編の TheRecordReplyManagerです。これRecordReplyManagerにより、記録または再生の必要性に応じて、API のプロキシ オブジェクトが提供されます。

    public class RecordReplyManager implements java.lang.reflect.InvocationHandler {

        private Object objOfApi;
        private boolean isForRecording;

        public static Object newInstance(Object obj, boolean isForRecording) {

            return java.lang.reflect.Proxy.newProxyInstance(
                    obj.getClass().getClassLoader(),
                    obj.getClass().getInterfaces(),
                    new RecordReplyManager(obj, isForRecording));
        }

        private RecordReplyManager(Object obj, boolean isForRecording) {
            this.objOfApi = obj;
            this.isForRecording = isForRecording;
        }


        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object result;
            if (isForRecording) {
                try {
                    System.out.println("recording...");
                    System.out.println("method name: " + method.getName());
                    System.out.print("method arguments:");
                    for (Object arg : args) {
                        System.out.print(" " + arg);
                    }
                    System.out.println();
                    result = method.invoke(objOfApi, args);
                    System.out.println("result: " + result);
                    RecordedInformation recordedInformation = new RecordedInformation();
                    recordedInformation.setMethodName(method.getName());
                    recordedInformation.setArgs(args);
                    recordedInformation.setReturnValue(result);
                    //persist your information

                } catch (InvocationTargetException e) {
                    throw e.getTargetException();
                } catch (Exception e) {
                    throw new RuntimeException("unexpected invocation exception: " +
                            e.getMessage());
                } finally {
                    // do nothing
                }
                return result;
            } else {
                try {
                    System.out.println("replying...");
                    System.out.println("method name: " + method.getName());
                    System.out.print("method arguments:");
                    for (Object arg : args) {
                        System.out.print(" " + arg);
                    }

                    RecordedInformation recordedInformation = new RecordedInformation();
                    recordedInformation.setMethodName(method.getName());
                    recordedInformation.setArgs(args);

                    //if your invocation information (this RecordedInformation) is found in the previously collected map, then return the returnValue from that RecordedInformation.
                    //if corresponding RecordedInformation does not exists then invoke the real method (like in recording step) and wrap the collected information into RecordedInformation and persist it as you like!

                } catch (InvocationTargetException e) {
                    throw e.getTargetException();
                } catch (Exception e) {
                    throw new RuntimeException("unexpected invocation exception: " +
                            e.getMessage());
                } finally {
                    // do nothing
                }
                return result;
            }
        }
    }

メソッド呼び出しを記録したい場合は、次のような API プロキシを取得するだけで済みます。

    MyApi realApi = new RealApi(); // using new or whatever way get your service implementation (API implementation)
    MyApi myApiWithRecorder = (MyApi) RecordReplyManager.newInstance(realApi, true); // true for recording
    myApiWithRecorder.getMySpouse("richard"); // to record getMySpouse
    myApiWithRecorder.getMyAge("parker"); // to record getMyAge
    ...

必要なものをすべて再生するには:

    MyApi realApi = new RealApi(); // using new or whatever way get your service implementation (API implementation)
    MyApi myApiWithReplayer = (MyApi) RecordReplyManager.newInstance(realApi, false); // false for replaying
    myApiWithReplayer.getMySpouse("richard"); // to replay getMySpouse
    myApiWithRecorder.getMyAge("parker"); // to replay getMyAge
    ...

そして、あなたは完了です!

編集: レコーダーとリプレーヤーの基本的な手順は、上記の方法で実行できます。これらの手順をどのように使用または実行するかは、あなた次第です。レコーダーとリプレーヤーのコード ブロックで好きなことを何でも実行でき、実装を選択するだけです。

于 2013-05-12T16:55:29.660 に答える
4

Yves Martin の回答にある懸念事項のいくつかを共有していると言うことで、これに接頭辞を付ける必要があります。

とはいえ、技術的な観点から見ると、これは興味深い問題であり、私はそれに取り組むことができませんでした. かなり一般的な方法でメソッド呼び出しをログに記録する要点をまとめました。そこでCallLoggingProxy定義されたクラスは、次のような使い方ができます。

Calendar original = CallLoggingProxy.create(Calendar.class, Calendar.getInstance());
original.getTimeInMillis(); // 1368311282470

CallLoggingProxy.ReplayInfo replayInfo = CallLoggingProxy.getReplayInfo(original);

// Persist the replay info to disk, serialize to a DB, whatever floats your boat.
// Come back and load it up later...

Calendar replay = CallLoggingProxy.replay(Calendar.class, replayInfo);
replay.getTimeInMillis(); // 1368311282470

API オブジェクトをCallLoggingProxy.createテスト メソッドに渡す前に でラップし、後でデータをキャプチャし、好みのシリアライゼーション システムを使用して永続化することを想像できます。後でテストを実行したい場合は、データをバックアップしてロードし、 を使用してデータに基づいて新しいインスタンスを作成し、CallLoggingProxy.replay代わりにそれをメソッドに渡すことができます。

Javaのネイティブはインターフェイスに対する動作に制限されているため、 JavassistCallLoggingProxyを使用して記述されています。これは一般的な使用例をカバーするはずですが、留意すべきいくつかの制限があります。Proxy

  • 宣言されたクラスはfinal、このメソッドによってプロキシできません。(簡単に修正できません。これはシステムの制限です)
  • 要点は、メソッドへの同じ入力が常に同じ出力を生成することを前提としています。(より簡単に修正できます。ReplayInfo単一の入力/出力ペアではなく、各入力の一連の呼び出しを追跡する必要があります。)
  • 要点はリモートでもスレッドセーフではありません (かなり簡単に修正できます。少し考えて努力する必要があります)。

明らかに、要点は概念の証明にすぎないため、十分にテストされていませんが、一般的な原則は正しいと思います。この種の目標を達成するために、より完全に焼き付けられたフレームワークが存在する可能性もありますが、そのようなものが存在する場合、私はそれを認識していません.

リプレイ アプローチを継続することにした場合は、うまくいけば、これで作業の方向性を示すことができます。

于 2013-05-11T23:13:10.303 に答える
3

これはAOP、アスペクト指向プログラミングで行うことができます。バイトコード操作によるメソッド呼び出しの傍受を可能にします。例を少し検索します。

ある場合にはこれで録音を行うことができ、別の場合には再生することができます。

ポインター:ウィキペディア、AspectJ、Spring AOP。

残念ながら、Java 構文から少し外れているため、単純な例は別の場所で探したほうがよいでしょう。解説付き。

単体テスト/記録されたデータを使用したオフライン テスト用のモック テスト フレームワークと組み合わせることができます。

于 2013-05-12T00:09:45.390 に答える
1
// pseudocode
class LogMethod {
   List<String> parameters;
   String method;
   addCallTo(String method, List<String> params):
       this.method = method;
       parameters = params;
   }
}

テスト メソッド内のすべての呼び出しの前に、リストLogMethodsと呼び出しを用意します。new LogMethod().addCallTo()

于 2013-05-11T21:25:07.453 に答える
1

API 呼び出しを再生するというアイデアは、イベント ソーシング パターンのユース ケースのように思えます。Martin Fowler の優れた記事がここにあります。これは、イベントを一連のオブジェクトとして記録し、保存するという優れたパターンです。その後、必要に応じて一連のイベントを再生できます。

Eventsourcedと呼ばれる Akka を使用したこのパターンの実装があり、必要なタイプのシステムを構築するのに役立つ場合があります。

于 2013-05-12T12:49:15.617 に答える