Подтвердить что ты не робот

Записывает метод вызовов в один сеанс для повторного воспроизведения в будущих тестовых сеансах?

У меня есть бэкэнд-система, в которой мы используем сторонний Java API для доступа из наших собственных приложений. Я могу получить доступ к системе как к обычным пользователям вместе с другими пользователями, но у меня нет божественных полномочий.

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

Итак, если мое приложение содержит строку в форме:

 Object b = callBackend(a);

Я хотел бы, чтобы фреймворк сначала зафиксировал, что callBackend() возвратил b, дал аргумент a, а затем, когда я делаю сухой прогон в любое более позднее время, скажите "эй, учитывая, что этот вызов должен вернуть b". Значения a и b будут одинаковыми (если нет, мы перезапустим шаг записи).

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

В какой структуре я должен искать это?


EDIT: Обратите внимание, что охотники за головами должны предоставить фактический код, демонстрирующий поведение, которое я ищу.

4b9b3361

Ответ 1

На самом деле вы можете создать такую ​​структуру или шаблон, используя шаблон прокси. Здесь я объясню, как вы можете это сделать, используя динамический шаблон прокси. Идея состоит в том, чтобы

  • Напишите прокси-менеджер, чтобы получить прокси-серверы рекордера и replayer API по запросу!
  • Напишите класс-оболочку для хранения собранной информации, а также реализуйте метод hashCode и equals этого класса-оболочки для эффективного поиска из Map как структура данных.
  • И, наконец, использовать прокси-сервер рекордера для записи и повторного прокси-сервера для воспроизведения.

Как работает рекордер:

  • вызывает реальный API
  • собирает информацию о вызове
  • сохраняет данные в ожидаемом контексте сохранения.

Как работает проигрыватель:

  • Соберите информацию о методе (имя метода, параметры)
  • Если собранная информация совпадает с ранее записанной информацией, верните ранее полученное возвращаемое значение.
  • Если возвращаемое значение не совпадает, сохраните собранную информацию (как вы хотели).

Теперь давайте посмотрим на реализацию. Если ваш 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!
        }
    }

Теперь вот основная часть, < <27 > . Этот 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
    ...

И ты готов!

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

Ответ 2

Я должен префикс этого, сказав, что я разделяю некоторые из проблем в ответе Ив Мартина: что такая система может оказаться разочаровывающей для работы и в конечном счете менее полезной, чем казалось бы, сначала краснея.

Тем не менее, с технической точки зрения, это интересная проблема, и я не мог не пойти на это. Я собрал gist, чтобы вести вызовы методов метода довольно обычным способом. Определенный там класс 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 и передать это вместо ваших методов.

CallLoggingProxy записывается с использованием Javassist, поскольку Java native Proxy ограничивается работой с интерфейсами. Это должно охватывать общий прецедент, но есть несколько ограничений, которые следует учитывать:

  • Объявленные классы final не могут быть проксированы этим методом. (Не легко устранить, это системное ограничение)
  • Суть предполагает, что один и тот же ввод метода всегда будет выдавать тот же результат. (Более легко устранить, ReplayInfo нужно будет отслеживать последовательности вызовов для каждого входа вместо одиночных пар ввода/вывода.)
  • Суть даже не удалена поточно (довольно легко исправляется, просто требуется небольшая мысль и усилие).

Очевидно, что суть - просто доказательство концепции, поэтому оно также не очень тщательно протестировано, но я считаю, что общий принцип звучит. Это также возможно там более полностью испеченная основа для достижения такой цели, но если такая вещь существует, я не знаю об этом.

Если вы решите продолжить подход к воспроизведению, то, надеюсь, этого будет достаточно, чтобы дать вам возможное направление для работы.

Ответ 3

У меня были те же потребности несколько месяцев назад для тестирования без регрессии при планировании тяжелого технического рефакторинга большого приложения и... Я не нашел ничего доступного в качестве рамки.

На самом деле переигровка может быть особенно сложной и может работать только в определенном контексте - никакое (или несколько) приложение со стандартной сложностью действительно может считаться апатридом. Это обычная проблема при тестировании кода сохранения с реляционной базой данных. Чтобы быть релевантным, полное исходное состояние системы должно быть восстановлено, и каждый шаг воспроизведения должен воздействовать на глобальное состояние таким же образом. Это становится проблемой, когда состояние системы распространяется на части, такие как базы данных, файлы, память... Предположим, что произойдет, если временная метка, взятая из системных часов, используется где-то!

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

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

Сначала для записи: вы можете использовать динамический прокси-интерфейс или аспектное программирование для перехвата вызова метода и для захвата состояния до и после вызова. Это может означать: сбрасывать соответствующие таблицы базы данных, копировать некоторые файлы, сериализовывать объекты Java в текстовом формате, таком как XML.

Затем сравните этот захват ссылок с новым прогоном. Это сравнение должно быть настроено так, чтобы исключать любые нерелевантные элементы из каждой части состояния, такие как идентификаторы строк, метки времени, имена файлов... для сравнения данных, в которых светит ваше добавленное значение.

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

Ответ 4

Это можно сделать с помощью AOP, аспектно-ориентированного программирования. Он позволяет перехватывать вызовы методов с помощью манипулирования байтовым кодом. Выполните поиск примеров.

В одном случае это может сделать запись в другом воспроизведении.

Указатели: wikipedia, AspectJ, Spring AOP.

К сожалению, немного перемещается за пределы синтаксиса java, и простой пример лучше искать в другом месте. С объяснением.

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

Ответ 5

вы можете посмотреть 'Mockito'

Пример:

//You can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);

//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

//following prints "first"
System.out.println(mockedList.get(0));

//following throws runtime exception
System.out.println(mockedList.get(1));

//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

после повторного воспроизведения каждого теста больше времени, и он вернет данные, которые вы ввели.

Ответ 6

// pseudocode
class LogMethod {
   List<String> parameters;
   String method;
   addCallTo(String method, List<String> params):
       this.method = method;
       parameters = params;
   }
}

Составьте список LogMethods и вызовите new LogMethod().addCallTo() перед каждым вызовом вашего тестового метода.

Ответ 7

Идея воспроизведения вызовов API звучит как пример использования шаблона источника событий. У Мартина Фаулера есть хорошая статья о нем здесь. Это хороший шаблон, который записывает события как последовательность объектов, которые затем сохраняются, затем вы можете воспроизводить последовательность событий по мере необходимости.

Существует реализация этого шаблона с использованием Akka, называемого Eventsourced, что может помочь вам построить тип требуемой системы.

Ответ 8

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

  • как извлечь моментальный снимок записанного объекта (объектов) (не только для объектов, реализующих Serializable)
  • как сгенерировать тестовый код сериализованного представления читаемым способом (не ограничиваясь только beans, примитивами и коллекциями)

Поэтому мне пришлось идти своим путем - с testrecorder.

Например, данный:

ResultObject b = callBackend(a);

...

ResultObject callBackend(SourceObject source) {
  ...
}

вам нужно будет только аннотировать метод следующим образом:

@Recorded
ResultObject callBackend(SourceObject source) {
  ...
}

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

  • Сериализация аргументов, результатов, состояния этого, исключений (полный граф объектов!)
  • поиск читаемого представления для построения объекта и сопоставления объектов
  • генерация теста из сериализованных данных
  • вы можете расширять записи до глобальных переменных, ввода и вывода с аннотациями

Пример для теста будет выглядеть следующим образом:

void testCallBackend() {
  //arrange
  SourceObject sourceObject1 = new SourceObject();
  sourceObject1.setState(...); // testrecorder can use setters but is not limited to them
  ... // setting up backend
  ... // setting up globals, mocking inputs

  //act
  ResultObject resultObject1 = backend.callBackend(sourceObject1);

  //assert
  assertThat(resultObject, new GenericMatcher() {
    ... // property matchers
  }.matching(ResultObject.class));
  ... // assertions on backend and sourceObject1 for potential side effects
  ... // assertions on outputs and globals
}

Ответ 9

Если я правильно понял вопрос, вы должны попробовать db4o.

Вы сохраните объекты с помощью db4o и позже восстановите их, чтобы выполнить mock и тесты JUnit.