Pull to refresh
43.87
Crystal Service Integration
Решения для сетевого ритейла: ПО и оборудование

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

Reading time 4 min
Views 17K


Добрый день! Полтора года назад моей команде пришлось протестировать 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. Всё построено на действиях и ожидаемых результатах. Некоторые экраны и компоненты вы даже не успеете увидеть — тем не менее они успевают проверяться вдоль и поперёк:

Tags:
Hubs:
+23
Comments 5
Comments Comments 5

Articles

Information

Website
www.crystals.ru
Registered
Founded
Employees
201–500 employees
Location
Россия