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

86462a07a43947d49b2226fd453807c1.jpgДобрый день!

В мире, в котором стоимость ошибки на этапе внедрения превышает в сотни и тысячи раз стоимость исправления на этапе разработки, нужно всегда искать ответ на вопрос: «а как это тестировать автоматически?» Вопросы автоматизации тестирования JavaFX приложений глобальная паутина практически не освещает. Но всё же удалось найти несколько интересных идей, и я хочу поделиться с вами своими наблюдениями.

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

1. Исходные данныеНабор библиотек: guava, testFx, hamcrest и JUnit.Я принципиально не буду описывать логику работы самого приложения, скажу только, что это калькулятор, написанный на скорую руку — постараемся максимально долго работать с ним, как с black-box. Тем не менее начну я с самого класса launcher-а приложения: public class CalculatorApp extends Application { private static Optional> callback = Optional.empty ();

public static void main (String[] args) { launch (args); }

@Override public void start (Stage primaryStage) throws Exception { BorderPane root = new BorderPane (); root.setCenter (new Calculator ()); Scene scene = new Scene (root); primaryStage.setScene (scene); primaryStage.show (); callback.ifPresent (o → o.call (root)); } public static void onLoad (Callback r) { CalculatorApp.callback = Optional.of®; } } Зачем нужен callback станет понятно чуть позже. Пока нам нужно знать о нём только это:

public interface Callback { void call (T arg); } Помимо launcher-а, как вы можете догадаться, есть Calculator.java — контроллер, Calculator.fxml — компоненты со всей иерархией, layout-ами и прочим, Calculator.css — стили, используемые компонентами нашей визуалки. В конечном счёте наш калькулятор выглядит как-то так:

803b6cc9a4f54ca59f367fddc1e79d0f.PNG

2. Инициализация теста public class FirstTest { private static GuiTest controller;

@BeforeClass public static void setUpClass () { CalculatorApp.onLoad (r → { controller = new GuiTest () { @Override protected Parent getRootNode () { return r; } }; });

FXTestUtils.launchApp (CalculatorApp.class); try { Thread.sleep (1000); } catch (InterruptedException e) { e.printStackTrace (); } } … Чтобы автоматизировать тестирование с использованием TestFX нам требуется GuiTest () — это абстрактный класс, содержащий в себе множество полезных методов. Он требует от нас реализации Parent getRootNode (). Callback передаёт в реализацию GuiTest реальный root. Этого достаточно для того, чтобы ходить рекурсивно по иерархии компонентов, что на самом деле TestFX и делает. Очень советую заглянуть в исходники библиотеки — там есть много интересного и сразу понятны принципы её работы.

FXTestUtils.launchApp (CalculatorApp.class); Ждать не обязательно — можно сделать более умное ожидание загрузки приложения, но для простоты у меня Thread.sleep (1000);

3. Методы В первую очередь нам понадобится научить наш движок нажимать УДАЛ. для использования в Before: private void clear () { controller.click («УДАЛ.»); } Да, именно так просто — и это только один из способов. На самом деле происходит плавное перемещение мышки и клик. Чтобы в будущем избежать ненужной траты времени на красивости можно перейти к пробрасыванию событий напрямую нужной ноде (но я оставлю медленный вариант, чтобы показать вам видео в динамике). А пробрасывание событий делается как-то так:

Event.fireEvent (your_node, new MouseEvent (MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0, MouseButton.PRIMARY, 1, true, true, true, true, true, true, true, true, true, true, null)); Итого мы имеем то, чего и добивались — очистка полей калькулятора (сброс), который будем производить перед каждым тестом:

@Before public void beforeTest () { clear (); } Аналогично реализуем метод, который накликает нам нужное число на калькуляторе.

public void click (int digit) { String numStr = Integer.toString (digit); for (int i = 0; i < numStr.length(); i++) { controller.click(String.valueOf(numStr.charAt(i))); } } Теперь я покажу более интересный вариант нажатий на различные контролы. Задача — научиться нажимать на +,-,*,/,=. Заглянем в нашу fxml и поймём, а чем таким уникальным отличаются эти компоненты.

У нас есть уникальные fx: id, которыми мы и воспользуемся. Для удобства создадим enumeration с операциями:

public enum Operation { ADD, SUBTRACT, MULTIPLY, DIVIDE, EQ; } Теперь создадим свою реализацию org.hamcrest.Matcher. Будем передавать нашу операцию в конструктор, а затем, приводя в нижний регистр, будем сравнивать с поступающими на вход объектами.

public class OperationMatcher implements Matcher { private Operation operation;

public OperationMatcher (Operation operation) { this.operation = operation; }

@Override public boolean matches (Object item) { if (item instanceof Labeled) { String expected = operation.toString ().toLowerCase (); String id = ((Labeled)item).getId (); if (id!= null) { if (expected.equals (id.toLowerCase ())) { return true; } } } return false; } … Конечно, тут много лишнего я написал, но это просто чтобы показать, что item — это в первую очередь node и к нему применимы различные проверки и приведения. Теперь мы можем воспользоваться методом GuiTest: public GuiTest click (Matcher matcher, MouseButton… buttons), а именно создадим метод:

private void perform (Operation operation) { Matcher matcher = new OperationMatcher (operation); controller.click (matcher, MouseButton.PRIMARY); } Итак, нам осталось проверять получающийся результат. То есть найти label (operation) и textField (input)… Никто не запрещает нам написать ещё matcher-ов — у GuiTest естественно есть метод поиска по matcher-у.

Однако я покажу другой способ, а именно поиск по styleClass (sleep вставил опять же для простоты — надо дождаться отрисовки):

public void checkDescriptionField (String expectedText) throws InterruptedException { Thread.sleep (200); Node result = controller.find (».operation»); String actualText = ((Labeled) result).getText (); Assert.assertEquals (expectedText.trim (), actualText.trim ()); }

public void checkInputField (String expectedText) throws InterruptedException { Thread.sleep (200); Node result = controller.find (».input»); String actualText = ((TextField) result).getText (); Assert.assertEquals (expectedText.trim (), actualText.trim ()); } Пришло время для написания простейших тестов на сложение и вычитание:

@Test public void testADD () throws InterruptedException { int digit1 = random.nextInt (1000); int digit2 = random.nextInt (1000);

click (digit1); checkDescriptionField (String.valueOf (digit1)); checkInputField (String.valueOf (digit1));

perform (Operation.ADD);

click (digit2); checkDescriptionField (digit1 + » + » + digit2); checkInputField (String.valueOf (digit2));

perform (Operation.EQ);

checkInputField (String.valueOf (digit1 + digit2) + »,00»); } @Test public void testSubstract () throws InterruptedException { int digit1 = random.nextInt (1000); int digit2 = random.nextInt (1000);

click (digit1); checkDescriptionField (String.valueOf (digit1)); checkInputField (String.valueOf (digit1));

perform (Operation.SUBTRACT);

click (digit2); checkDescriptionField (digit1 + » − » + digit2); checkInputField (String.valueOf (digit2));

perform (Operation.EQ);

checkInputField (String.valueOf (digit1 — digit2) + »,00»); } »,00» для простоты — понятно, что надо делать через Formatter-ы, понятно, что надо заменять Thread.sleep на ожидание, а клики на прокидывание event-ов — тогда тесты начнут летать. Но это уже выходит за рамки рассказа про возможности TestFX.

Кстати, я рассказал вам про TestFX третьей версии, — буквально несколько недель назад вышла alpha версия 4.0.1. Особенно интересна часть testfx-legacy, но об этом я напишу, когда погружусь глубже в исходники, — статью опубликую тут на английском.

Обещанное видео запуска написанных тестов ниже:

[embedded content]

© Habrahabr.ru