Автоматическое тестирование Java Swing приложений



    Добрый день! Полтора года назад моей команде пришлось протестировать Java Swing приложение, которое могло иметь разные визуализации, натянутые на общий процесс. Статей тогда по этой теме было немного, конкретные решения отсутствовали вообще. TestComplete и прочие скриптовые технологии (да простят меня сторонники TestComplete) использовать не хотелось, так как приложение должно иметь гибкую архитектуру, расширяемую и изменяемую в рамках Agile процесса.

    Сутки поиска в Google, анализ десятков примеров и технологий привели меня к двум возможным вариантам:
    • Fest
    • Jemmy

    Не погружаясь в глубины глубин сравнения, я выбрал Fest библиотеку. С её помощью и, конечно, Junit, Mockito мы начали тестировать наше приложение. Об этом и расскажу ниже.

    Вы, конечно, можете начать с чтения документации из Getting Started, и использовать работу через FrameFixture — это всё хорошо описано здесь, но если задачи выходят за рамки поиска/нажимания на стандартные Java Swing компоненты, то ниже, на примере конкретных задач, я покажу как это делается.

    Начнём с робота, а именно org.fest.swing.core.Robot:
    Robot robot = BasicRobot.robotWithCurrentAwtHierarchy();
    

    Робот умеет кликать компоненты, фокусироваться на них, работать с курсором мышки, выполнять разные проверки. Но начнём мы с реализации ComponentFinder, который робот нам любезно предоставляет. А чтобы это было интересно, рассмотрим всё на примере. В этой статье я не буду предлагать архитектурные решения, предлагать реализацию PageObject паттерна для Swing, — скорее постараюсь просто объяснить на примере, как использовать библиотеку. Архитектуру уже можете придумывать свою – задачи всегда разные.

    Допустим, у нас есть JFrame с тремя кастомными кнопками. Допустим, требуется их найти, проверить, что тексты на них «Купить», «Продать», «Удалить» соответственно и что кнопка «Удалить» !isEnabled().

    Для этого нам понадобится:
    ComponentFinder finder = robot.finder();
    

    У него есть много методов, таких как findByLabel, findByName. Но мы обратим наше внимание на:
    • finder.find(ComponentMatcher matcher) – возвращает Component
    • finder.findAll(ComponentMatcher matcher) – возвращает Collection.

    Есть варианты методов 1 и 2 с передачей generic matcher-а – иногда бывает полезно. Но так как обычно мы знаем, что ищем, то в основном используются 1 и 2.

    Интерфейс ComponentMatcher представляет из себя по сути один метод:
    boolean matches(Component c)
    

    Нам понадобится реализация для поиска нашей кастомной кнопки по тексту на ней:
    public class CaptionMatcher implements ComponentMatcher {
        private String expectedCaption;
        private String actualComponentCaption;
    
    public CaptionMatcher(String expectedCaption) {
        this.expectedCaption = expectedCaption;
    }
    
    @Override
    public boolean matches(Component comp) {
        if (comp != null && expectedCaption != null && comp instanceof CustomButton) {
            actualComponentCaption = ((CustomButton) comp).getText();
            if (expectedCaption.equals(actualComponentCaption)) {
                return true;
            }
        }
        return false;
    }
    
    public String getExpectedCaption() {
        return expectedCaption;
    }
    
    public void setExpectedCaption(String expectedCaption) {
        this.expectedCaption = expectedCaption;
    }
    
    }
    

    Теперь попробуем найти нашу первую кнопку:
    finder.find(new CaptionMatcher("Купить"));
    

    Если она не будет найдена, то вернётся ComponentLookupException. Так же мы можем найти и вторую кнопку, и третью, изменив expectedCaption у матчера. С третьей кнопкой можно поступить по-разному. Если достаточно часто приходится проверять «задизэйбленности» кнопок, то следует перегрузить конструктор CaptionMatcher и добавить в matches проверку на enable/disable.

    Если редко, то можно просто так:
    CustomButton btnDel = (CustomButton) finder.find(new CaptionMatcher("Удалить"));
    Assert.assertTrue(!btnDel.isEnabled());
    

    Можно было пойти другим путём – сделать ClassMatcher, в котором matches возвращает true, если компонент instanceof CustomButton. И искать, используя findAll. Тогда мы бы получили все кнопки и затем уже, пробежавшись по ним, привели бы к классу наших кнопок и проверили бы всё, что нужно.

    В этом пункте абсолютно необходима одна ремарка – потоки по отрисовке Swing никто не отменял, и если вы начинаете искать компонент, пока он ещё не успел отрисоваться, то получите ComponentLookupException. Потому best practice, на мой взгляд, оборачивать поиск компонента в цикл и искать компонент с некоторым timeout, перехватывая все ошибки поиска. Если уж за выделенный timeout компонент не нашёлся, то считать, что его нет и прокидывать ComponentLookupException выше.

    Мы научились находить компоненты. Теперь по поводу нажатий. Опыт показал, что лучше прокидывать ActionEvent напрямую компоненту, чем пытаться, используя робота, кликать мышкой. Во-первых, при таком подходе, можно спокойно продолжать работать пока гоняются тесты, во-вторых, быстрее бросить event, чем кликать. У нас этот event реализован на уровне самого компонента — оформлено в виде публичного метода doClick().

    Для того, чтобы продемонстрировать Fest в действии:

    1. Ниже ссылка на репу — для наглядности я набросал несколько тестов, проверяющих сложение, вычитание и умножение в калькуляторе. Обращу внимание, что между нажатиями вставлен Thread.sleep(100);
    Чтобы ускорить, лучше как-нибудь оформить «ожидаемый» результат действия. Не только в калькуляторе, но и в любом другом проекте — любое действие желательно связывать с ожидаемым результатом.

    bitbucket.org/stovmasyan/calcexample/

    Тут видео запуска этих тестов:



    2. А вот тут видео, как автоматически тестируется наш продукт — касса self-checkout. Всё построено на действиях и ожидаемых результатах. Некоторые экраны и компоненты вы даже не успеете увидеть — тем не менее они успевают проверяться вдоль и поперёк:

    Метки:
    • +23
    • 13,7k
    • 5
    Кристалл Сервис 34,12
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 5
    • +1
      А проверку на то что найденный компонент полностью виден на экране, т.е. его никто не перекрывает и он не выехал за пределы экрана, вы не делаете?
      • +2
        Лишь иногда. У нас прекрасное/близкое к идеальному MVC, потому за всё время проекта не было ни одного прецедента наложения/«выезжания». Так что вставлять эти проверки — только усложнять и замедлять тесты. По идее компоненты, которые находятся за другими или вне видимой области не попадают в dirty region и, думаю, у AWT можно эту информацию получить. Но уверенности нет, надо попробовать.
        • +2
          Первое, что нашёл,
          docs.oracle.com/javase/1.5.0/docs/api/javax/swing/RepaintManager.html

          Rectangle	getDirtyRegion(JComponent aComponent) 
          


          Возможно, это то, что нужно.
        • 0
          Не погружаясь в глубины сравнения...

          А это как раз таки самое интересное, почему fest, а не jemmy?
          • +1
            Ну когда нет реальных статей, сравнивающих два движка, а решение надо принимать быстро, то, уж извините…
            Уже позже, пытаясь сравнить, — по ощущениям проход по свинговому дереву у Jemmy не оптимизирован и работает медленнее, чем у Fest (это мнение коллеги, который работает с Jemmy). Поэтому, как я понял, те, кто используют Jemmy, не всегда, но стараются при наличии множества проверок на одном слепке дерева работать с ним в памяти. С Fest таких проблем нет и можно для каждого компонента в отдельности запускать поиск, — никакой видимой потери в скорости не наблюдается.

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

          Самое читаемое