Статьи

Проверка на инжекцию неисправностей — первые шаги с JBoss Byteman


Тестирование на инжекцию неисправностей
[1] является очень полезным элементом комплексной стратегии тестирования, поскольку позволяет вам сконцентрироваться на области, которую может быть трудно проверить; способ, которым тестируемое приложение может обрабатывать исключения.

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

JBoss Byteman

Недавно я нашел инструмент для инжекции байт-кода, который позволяет автоматизировать тесты на инжекцию ошибок. JBoss Byteman
[2]это проект с открытым исходным кодом, который позволяет вам писать сценарии в Java-подобном синтаксисе для вставки событий, исключений и т. д. в код приложения.

Байтмен версии 1.1.0 доступен для загрузки с:
http://www.jboss.org/byteman — загрузка включает в себя руководство программиста. Здесь также есть пользовательский форум для того, чтобы задавать вопросы:
http://www.jboss.org/index.html?module=bb&op=viewforum&f=310 и проект JIRss.org JIRA для представленных проблем и запросов функций здесь:
https: / /jira.jboss.org/jira/browse/BYTEMAN

Простой пример

В оставшейся части этого поста описан простой пример в масштабе классического примера «Привет, мир»: использование Byteman для вставки исключения в работающее приложение.

Давайте начнем с определения исключения, которое мы добавим в наше приложение:

  package sample.byteman.test;

/**
* Simple exception class to demonstrate fault injection with byteman
*/

public class ApplicationException extends Exception {

private static final long serialVersionUID = 1L;
private int intError;
private String theMessage = "hello exception - default string";

public ApplicationException(int intErrNo, String exString) {
intError = intErrNo;
theMessage = exString;
}

public String toString() {
return "**********ApplicationException[" + intError + " " + theMessage + "]**********";
}

} /* class */

Здесь нет ничего сложного, но обратите внимание на строку, которая передается конструктору исключений в строке 13.

Теперь давайте определим наш класс приложения:

  package sample.byteman.test;

/**
* Simple class to demonstrate fault injection with byteman
*/

public class ExceptionTest {

public void doSomething(int counter) throws ApplicationException {
System.out.println("called doSomething(" + counter + ")");
if (counter > 10) {
throw new ApplicationException(counter, "bye!");
}
System.out.println("Exiting method normally...");
} /* doSomething() */

public static void main(String[] args) {
ExceptionTest theTest = new ExceptionTest();
try {
for (int i = 0; i < 12; i ++) {
theTest.doSomething (i);
}
} catch (ApplicationException e) {
System.out.println("caught ApplicationException: " + e);
}
}

} /* class*/

Приложение создает экземпляр ExceptionTest в строке 18, затем запускает метод doSomething в цикле, пока счетчик не станет больше 10. Затем он вызывает исключение, которое мы определили ранее. Когда мы запускаем приложение, мы видим этот вывод:

java -classpath bytemanTest.jar sample.byteman.test.ExceptionTest
called doSomething(0)
Exiting method normally...
called doSomething(1)
Exiting method normally...
called doSomething(2)
Exiting method normally...
called doSomething(3)
Exiting method normally...
called doSomething(4)
Exiting method normally...
called doSomething(5)
Exiting method normally...
called doSomething(6)
Exiting method normally...
called doSomething(7)
Exiting method normally...
called doSomething(8)
Exiting method normally...
called doSomething(9)
Exiting method normally...
called doSomething(10)
Exiting method normally...
called doSomething(11)
caught ApplicationException: **********ApplicationException[11 bye!]**********

ХОРОШО. Пока ничего особенного. Давайте сделаем вещи более интересными, написав правило Byteman для вставки исключения, прежде чем метод doSomething сможет распечатать любой вывод. Наш скрипт Byteman выглядит так:

   #
# A simple script to demonstrate fault injection with byteman
#
RULE Simple byteman example - throw an exception
CLASS sample.byteman.test.ExceptionTest
METHOD doSomething(int)
AT INVOKE PrintStream.println
BIND buffer = 0
IF TRUE
DO throw sample.byteman.test.ApplicationException(1,"ha! byteman was here!")
ENDRULE

  • Строка 4 — ПРАВИЛО определяет начало ПРАВИЛА. Следующий текст в этой строке не выполнен
  • Строка 5 — Ссылка на класс заявки на получение инъекции
  • Строка 6 — И метод в этом классе. Обратите внимание, что если бы мы написали эту строку как «METHOD doSomething», правило соответствовало бы любой сигнатуре метода soSomething.
  • Строка 7 — наше правило сработает при вызове метода PrintStream.println
  • Строка 8 — значения определения BIND для переменных, на которые можно ссылаться в теле правила, — в нашем примере получатель вызова метода doSomething, вызвавшего правило, идентифицируется ссылкой на параметр $ 0
  • Строка 9 — Правило должно включать предложение IF — в нашем примере это всегда верно
  • Строка 10 — когда правило срабатывает, мы бросаем исключение — обратите внимание, что мы передаем строку конструктору исключений

Теперь, прежде чем пытаться запустить этот прогон, мы должны проверить его синтаксис. Для этого мы встраиваем наше приложение в .jar (в нашем случае bytemanTest.jar) и используем bytemancheck.sh.

sh bytemancheck.sh -cp bytemanTest.jar byteman.txt
checking rules in sample_byteman.txt
TestScript: parsed rule Simple byteman example - throw an exception
RULE Simple byteman example - throw an exception
CLASS sample.byteman.test.ExceptionTest
METHOD doSomething(int)
AT INVOKE PrintStream.println
BIND buffer : int = 0
IF TRUE
DO throw (1"ha! byteman was here!")

TestScript: checking rule Simple byteman example - throw an exception
TestScript: type checked rule Simple byteman example - throw an exception

TestScript: no errors

Как только мы получим чистый результат, мы можем запустить приложение с Byteman. Для этого мы запускаем приложение и указываем дополнительный аргумент для команды java. Обратите внимание, что Byteman требует JDK 1.6 или новее.

java -javaagent:/opt/Byteman_1_1_0/build/lib/byteman.jar=script:sample_byteman.txt -classpath bytemanTest.jar sample.byteman.test.ExceptionTest

И результат:

caught ApplicationException: **********ApplicationException[1 ha! byteman was here!]**********

Теперь, когда скрипт работает, давайте улучшим его!

Давайте внимательнее посмотрим, как мы привязываемся к параметру метода. Если мы изменим сценарий следующим образом:

   #
# A simple script to demonstrate fault injection with byteman
#
RULE Simple byteman example - throw an exception
CLASS sample.byteman.test.ExceptionTest
METHOD doSomething(int)
AT INVOKE PrintStream.println
BIND counter = $1
IF TRUE
DO throw sample.byteman.test.ApplicationException(counter,"ha! byteman was here!")
ENDRULE

В строке 8 предложение BIND теперь ссылается на параметр метода int по индексу с использованием синтаксиса $ 1. Это изменение делает значение доступным внутри тела правила, позволяя нам использовать имя «counter». Затем значение счетчика передается в качестве аргумента в конструктор класса ApplicationException. Эта новая версия правила демонстрирует, как мы можем использовать локальное состояние, полученное из метода триггера,

для создания нашего объекта исключения.

Но подождите, это еще не все! Давайте использовать значение «counter» в качестве счетчика.

Полезно иметь возможность вызывать исключение при первом вызове метода. Но еще более полезно иметь возможность вызывать исключение при выбранном вызове метода. Давайте добавим тест для этого значения счетчика в скрипт:

   #
# A simple script to demonstrate fault injection with byteman
#
RULE Simple byteman example 2 - throw an exception at 3rd call
CLASS sample.byteman.test.ExceptionTest
METHOD doSomething(int)
AT INVOKE PrintStream.println
BIND counter = $1
IF counter == 3
DO throw sample.byteman.test.ApplicationException(counter,"ha! byteman was here!")
ENDRULE

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

Еще одна вещь — изменение сценария для запущенного процесса

Пока все хорошо. Мы смогли внедрить ошибку / исключение в наше работающее приложение и даже указать, в какой итерации цикла он происходит. Предположим, однако, что мы хотим изменить значение в скрипте byteman, пока приложение работает? Нет проблем! Вот как.

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

    public void doSomething(int counter) throws ApplicationException {

BufferedReader lineOfText = new BufferedReader(new InputStreamReader(System.in));
try {
System.out.println("Press <return>");
String textLine = lineOfText.readLine();
} catch (IOException e) {
e.printStackTrace();
}

System.out.println("called doSomething(" + counter + ")");
if (counter > 10) {
throw new ApplicationException(counter, "bye!");
}
System.out.println("Exiting method normally...");
}

Если мы запустим эту версию приложения, мы увидим вывод примерно так:

Press <return>

called doSomething(0)
Exiting method normally...
Press <return>

called doSomething(1)
Exiting method normally...
Press <return>

called doSomething(2)
Exiting method normally...
caught ApplicationException: **********ApplicationException[3 ha! byteman was here!]**********

Давайте снова запустим приложение, но на этот раз не нажимайте <return>. Пока приложение ожидает ввода, создайте копию сценария byteman. В этой копии измените предложение IF, чтобы счетчик цикла был установлен на другое значение, скажем, «5». Затем откройте второе окно командной оболочки и введите эту команду:

Byteman_1_1_0/bin/submit.sh sample_byteman_changed.txt

Затем вернитесь в первое окно командной оболочки и начните нажимать return, и вы увидите следующий вывод:

Press <return>
redefining rule Simple byteman example - throw an exception

called doSomething(0)
Exiting method normally...
Press <return>

called doSomething(1)
Exiting method normally...
Press <return>

called doSomething(2)
Exiting method normally...
Press <return>

called doSomething(3)
Exiting method normally...
Press <return>

called doSomething(4)
Exiting method normally...
caught ApplicationException: **********ApplicationException[5 ha! byteman was here!]**********

Таким образом, мы смогли изменить значение в исходном скрипте byteman, не останавливая тестируемое приложение!

Подводные камни в пути

Некоторые из ошибок новичка, которые я сделал по пути, были:

  • Каждое ПРАВИЛО нуждается в предложении IF — даже если вы хотите, чтобы правило всегда выполнялось
  • Методы, на которые ссылается RULE, не могут быть статическими — если они статические, то нет ссылки на $ 0 (иначе) на ссылку
  • Да, у меня было несколько ошибок и некоторые опечатки первые несколько раз, когда я пытался это сделать. Проверка синтаксиса всегда мой лучший друг. 😉

Заключение Мысли

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

Ссылки

[1]
http://en.wikipedia.org/wiki/Fault_injection

[2]
http://www.jboss.org/byteman

(Особая благодарность Эндрю Динну за помощь! 😉