Теория и практика Java: Повышение эффективности тестирования, часть 3

Проверка ограничений проектирования при помощи аспектов

В первых двух статьях данной серии описано, каким образом инструментальные средства статического анализа, например, FindBugs, могут усилить управление качеством ПО, нацелив ресурсы разработки не на отдельные случаи, а на целые классы ошибок. В данной статье, посвященной тестированию, Брайан Гетц исследует еще одну технологию устранения ошибок, нарушающих правила проектирования: аспекты.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



18.01.2007

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

Объединение методик тестирования

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

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

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


Простой аспект тестирования

Инструменты статического анализа, например FindBugs, проверяют код без его выполнения, а аспектно-ориентированные инструменты предлагают и статическую и динамическую обработку классов. Статические аспекты могут генерировать предупреждения или ошибки во время компиляции, а динамические аспекты могут добавлять в ваши классы код с контролем ошибок.

В части 1 мной был предложен простой детектор для обнаружения обращений к System.gc(), которые могут скрываться в библиотеках. Множество шаблонов ошибок, которые могут быть обнаружены статическим анализом, включая данный, могут также быть обнаружены и аспектами. Наличие обоих средств в вашем арсенале может быть выгодным, так как в зависимости от конкретного шаблона ошибки, его легче обнаружить либо при помощи статического анализа, либо при помощи аспектов.

В листинге 1 показан простой динамический аспект, отправляющий AssertionError когда собираются вызвать System.gc(). (Так как для детекторов ошибок, подобных данному, очень важно обнаруживать ошибки не только в вашем коде, но и в библиотеках, которыми пользуется ваш код, то вам придется указать инструментальным средствам анализировать и эти библиотеки.)

Листинг 1. Динамический аспект для выполнения правила "не вызова System.gc()"
public aspect GcAspect {
    pointcut gcCalls() : call(void java.lang.System.gc());
 
    before() : gcCalls() {
        throw new AssertionError("Don't call System.gc!");
    }
}

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

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

Листинг 2. Статический аспект для выполнения правила "не вызова System.gc()"
public aspect StaticGcAspect {
    pointcut gcCalls() : call(void java.lang.System.gc());

    declare error : gcCalls() : "Don't call System.gc!";
}

Поиск нарушений правила одного потока Swing

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

Правило одного потока Swing гласит:

Компоненты и модели Swing должны создаваться, изменяться и выполняться исключительно из потока диспетчеризации событий.

Более ранние формулировки правила одного потока разрешали обращаться к моделям и компонентам Swing из других потоков до их выполнения на экране. Но так как данный подход приводил к возникновению проблем с поддержкой потоков, то правило сделали более жестким. При помощи метода SwingUtilities.isEventDispatchThread() Swing предоставляет средство для получения ответа на вопрос "является ли текущий поток потоком диспетчеризации событий?" Таким образом, если бы нам пришлось добавлять код перед каждым вызовом метода Swing, проверяя что запрос сделан из соответствующего потока, и если нет, то отправлять AssertionError, то при тестировании мы бы смогли отследить нарушения правила одного потока и не позволить им внести путаницу.

В листинге 3 показан аспект, который может находить нарушения правила одного потока. Он состоит из двух частей: списка методов, которые не должны вызываться извне потока событий и кода, который необходимо вставить перед каждым вызовом одного из данных методов. Совет относительно добавляемого кода достаточно прост: проверьте, является ли текущий поток именно потоком событий и в противном случае отправьте AssertionError. Данный аспект реализует все обращения к методам из пакетов Swing и любых пакетов из классов, расширяющих основные классы Swing (например, для захвата компонентов и моделей, подготовленных пользователями). Однако он исключает методы в тех классах, к которым можно, как известно (или требуется), безопасно обращаться из множества потоков. Данный список безопасных методов не является исчерпывающим, так как создание полного списка потребовало бы дополнительных временных затрат на нахождение всех поточно-ориентированных методов в Swing Javadoc.

Листинг 3. Аспект для обеспечения выполнения правила одного потока Swing
public aspect SwingThreadAspect {

 pointcut swingMethods() : call(* javax.swing..*.*(..))
        || call(javax.swing..*.new(..));
 
 pointcut extendsSwing() : call(* javax.swing.JComponent+.*(..))
        || call(* javax.swing..*Model+.*(..))
        || call(* javax.swing.text.Document+.*(..));
 
 pointcut safeMethods() : call(void JComponent.revalidate())
        || call(void JComponent.invalidate(..))
        || call(void JComponent.repaint(..))
        || call(void add*Listener(EventListener+))
        || call(void remove*Listener(EventListener+))
        || call(boolean SwingUtilities.isEventDispatchThread())
        || call(void SwingUtilities.invokeLater(Runnable))
        || call(void SwingUtilities.invokeAndWait(Runnable))
        || call(void JTextPane.replaceSelection(..))
        || call(void JTextPane.insertComponent(..))
        || call(void JTextPane.insertIcon(..))
        || call(void JTextPane.setLogicalStyle(..))
        || call(void JTextPane.setCharacterAttributes(..))
        || call(void JTextPane.setParagraphAttributes(..));
 
 pointcut edtMethods() : (swingMethods() || extendsSwing()) && !safeMethods();
 
 before() : edtMethods() {
  if (!SwingUtilities.isEventDispatchThread())
   throw new AssertionError(thisJoinPointStaticPart.getSignature() 
     + " called from " + Thread.currentThread().getName());
 }
}

В pointcut (срез точек) swingMethods() содержатся все вызовы методов в пакете javax.swing, включая конструкторы. В pointcut extendsSwing() представлены все вызовы методов в классах, расширяющих JComponent или любой из классов моделей Swing. В pointcut safeMethods() представлены (некоторые) методы Swing, к которым можно безопасно обращаться из любого потока.

SwingThreadAspect неидеален, но в этом нет ничего страшного. Pointcut (срез точек) safeMethods() не перечисляет всех поточно-ориентированных методов, и вероятно, что pointcut extendsSwing() не содержит всех часто расширяемых классов Swing. Но ведь мы используем его не для создания, а для тестирования. Он находит ошибки, не вынуждая нас создавать новые тесты для каждой программы, и это очень важно. И, как и большинство детекторов ошибок, он, вероятно, найдет ошибки в тех программах, которые вы считали абсолютно корректными.


Загрузка отладочных объектов

Еще одним вариантом применения аспектов является загрузка "отладочных" версий класса для обычной версии. Зачастую создается отладочная версия класса, например, с большим числом регистраций или проверок ошибок, которая не подходит для использования в производстве вследствие побочных эффектов или производительности. А загрузка отладочной версии, когда это необходимо, может быть достаточно трудоемким и способствовать ошибкам. Если объект конкретизируется через свой конструктор, то вам придется найти в коде все места, в который вызывается конструктор. Обычная технология уменьшения неудобств, причиняемых изменением всех вызовов конструктора, заключается в конкретизации объектов вместо заводских настроек, а заводские настройки, используемые исключительно с целью выбора между серийной и отладочной версиями, могут все усложнить или добавить бреши в систему безопасности или надежности работы.

Если цель заключается в том, чтобы сказать, что "всякий раз когда нам нужно конкретизировать Foo, вместо него конкретизируйте DebuggingFoo ," то аспекты предоставляют очень простой механизм надежной реализации без внесения изменений в программу. В качестве примера в листинге 4 показан аспект, помогающий обнаруживать тупиковые ситуации, заменяя все экземпляры ReentrantLock на DebuggingLock. (Обратите внимание, что AspectJ модифицирует вызовы только в пределах того кода, который вы указали компилятору AspectJ; сами экземпляры ReentrantLock в библиотеках классов Java™ изменяться не будут, если только Вы не приложите все усилия, чтобы также внести свои аспекты в библиотеки инструментального комплекса.)

Листинг 4. Аспект, заменяющий все экземпляры ReentrantLock на DebuggingLock
public aspect ReentrantLockAspect {

  pointcut newLock() : call(ReentrantLock.new());

  pointcut newLockFair(boolean fair) : 
    call(ReentrantLock.new(boolean)) && args(fair);
		 
  ReentrantLock around() : newLock() {
    return new DebuggingLock();
  }
  ReentrantLock around(boolean fair) : newLockFair(fair) {
    return new DebuggingLock (fair);
  }
}

Что касается Java SE 6, то обнаружение тупиковых ситуаций во время прогона выполняется по запросу либо через интерфейс ThreadMXBean в java.lang.management, либо при запросе на выполнение дампа потока. В листинге 5 показана возможная реализация DebuggingLock, выполняющая поиск тупиковых ситуаций при каждой блокировке, что в свою очередь позволяет быстрее предупреждать вас о тупиковых ситуациях. Выполнение блокировок уступает ReentrantLock, так как проделывается больше работы при каждой попытке блокирования. Следовательно, данный подход не пригоден для использования в производстве. (Кроме того, синхронизация, присущая поддержке структуры данных waitingFor может нарушить временные соотношения Вашего приложения, изменяя правдоподобие тупиковой ситуации.)

Листинг 5. Отладочная версия ReentrantLock, отправляющая AssertionError при обнаружении тупиковой ситуации
public class DebuggingLock extends ReentrantLock {
    private static ConcurrentMap<Thread, DebuggingLock> waitingFor 
        = new ConcurrentHashMap<Thread, DebuggingLock>();

    public DebuggingLock() { super(); }
    public DebuggingLock(boolean fair) { super(fair); }

    private void checkDeadlock() {
        Thread currentThread = Thread.currentThread();
        Thread t = currentThread;
        while (true) {
            DebuggingLock lock = waitingFor.get(t);
            if (lock == null || !lock.isLocked())
                return;
            else {
                t = lock.getOwner();
                if (t == currentThread)
                    throw new AssertionError("Deadlock detected");
            }
        }
    }
    public void lock() {
        if (tryLock())
            return;
        else {
            waitingFor.put(Thread.currentThread(), this);
            try {
                checkDeadlock();
                super.lock();
            }
            finally {
                waitingFor.remove(Thread.currentThread());
            }
        }
    }
}

Для того чтобы версия DebuggingLock, представленная в листинге 5, оказалась полезной, при тестировании ваша программа действительно должна зайти в тупик. Так как тупиковые ситуации зачастую зависят от времени и окружения, то описанный в листинге 5 подход может оказаться не полным. В листинге 6 показана еще одна версия DebuggingLock, которая обнаруживает не только возникновение тупиковой ситуации, но и возникала ли данная пара блокировок когда-либо в противоречивом запросе нескольких потоков. При каждом возникновении блокировки, просматривается набор уже захваченных блокировок, и для каждой, запоминается, что некоторый поток запрашивал эти блокировки перед данной. Перед тем, как попытаться запросить блокировку, метод lock() просматривает уже захваченные блокировки и отправляет AssertionError, если какая-либо из этих блокировок возникала после данной. Пространственные затраты данной реализации гораздо больше, чем для предыдущей версии (так как необходимо хранить данные отслеживания всех блокировок, которые возникали перед данной блокировкой), однако она позволяет обнаруживать более широкий диапазон ошибок. Она не находит все возможные ошибки - только те, которые являются результатом противоречивых запросов между двумя конкретными блокировками.

Листинг 6. Альтернативный вариант DebuggingLock, обнаруживающий противоречивые запросы на блокировку, даже если они не привели к возникновению тупиковой ситуации
public class OrderHistoryLock extends ReentrantLock {
    private static ThreadLocal<Set<OrderHistoryLock>> heldLocks = 
      new ThreadLocal<Set<OrderHistoryLock>>() {
        public Set<OrderHistoryLock> initialValue() {
            return new HashSet<OrderHistoryLock>();
        }
    };
    private final Map<Lock, Boolean> predecessors 
    		 = new ConcurrentHashMap<Lock, Boolean>();
  
    public OrderHistoryLock() { super(); }
    
    public OrderHistoryLock(boolean fair) { super(fair); }

    public void lock() {
    		 boolean alreadyHeld = isHeldByCurrentThread();
        for (OrderHistoryLock lock : heldLocks.get()) {
            if (lock.predecessors.containsKey(this))
                throw new AssertionError("Possible deadlock between " 
                  + this + " and " + lock);
            else if (!alreadyHeld) 
                predecessors.put(lock, Boolean.TRUE);
        }
        super.lock();
        heldLocks.get().add(this);
    }
    public void unlock() {
        super.unlock();
        if (!isHeldByCurrentThread()) 
        		 heldLocks.get().remove(this);
    }
}

Заключение

Все описанные аспекты относятся к категории аспектов усиления стратегий. Некоторые стратегии входят в состав проекта вашего приложения, например, "данные методы должны вызываться только из класса X" или "ни при каких условиях не использовать System.out или System.err." Другие политики являются частью условий интерфейса для API, например, правило одного потока Swing или требование, что EJBs не должны создавать потоков или обращаться к AWT. Во всех этих случаях, вы можете применять аспекты в разработке и тестировании для обнаружения непреднамеренного нарушения данных политик. Так или иначе, использование аспектов в работе является хорошим дополнением к имеющимся у вас инструментальным средствам тестирования.

Ресурсы

Научиться

Получить продукты и технологии

  • FindBugs: Загрузите FindBugs и попробуйте его на своем коде.
  • AspectJ: Загрузите компилятор AspectJ и подключаемое расширение Eclipse для создания и использования аспектов в вашем проекте.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=188975
ArticleTitle=Теория и практика Java: Повышение эффективности тестирования, часть 3
publish-date=01182007