Filed under: BDD, Cucumber, Test automation, — Tags: CXF, Cucumber-jvm, JSF, JUnit, Jersey, MVC, Model view controller, REST, RESTAssured, RESTFul, Selenium, Soap, Swing,, Swinggui, WebDriver, Wicket — Thomas Sundberg — 2012-11-01
Previous - A Wicket web application
A Java Swing application is yet another graphical user interface that can be attached on top of the model developed earlier. The project is divided in the same way as earlier, in two parts. The only large difference here is the support class. It need to be adapted for a Swing user interface. Another difference is of obviously that the GUI is developed using Swing. But that has actually a rather small impact on the verification.
The feature is the same:
src/test/resources/se/waymark/rentit/Rent.feature
Feature: Rental cars should be possible to rent to gain revenue to the rental company. As an owner of a car rental company I want to make cars available for renting So I can make money Scenario: Find and rent a car Given there are 18 cars available for rental When I rent one Then there will only be 17 cars available for rental
The glue code to connect the feature above with Cucumber is also the same:
src/test/java/se/waymark/rentit/RunCukesIT.java
package se.waymark.rentit; import cucumber.api.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class RunCukesIT { }
The steps are also identical:
src/test/java/se/waymark/rentit/steps/RentStepdefs.java
package se.waymark.rentit.steps; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class RentStepdefs { private RentACarSupport rentACarSupport = new RentACarSupport(); @Given("^there are (\\d+) cars available for rental$") public void there_are_cars_available_for_rental(int availableCars) throws Throwable { rentACarSupport.createCars(availableCars); } @When("^I rent one$") public void rent_one_car() throws Throwable { rentACarSupport.rentACar(); } @Then("^there will only be (\\d+) cars available for rental$") public void there_will_be_less_cars_available_for_rental(int expectedAvailableCars) throws Throwable { int actualAvailableCars = rentACarSupport.getAvailableNumberOfCars(); assertThat(actualAvailableCars, is(expectedAvailableCars)); } }
The first interesting difference is the help class. It now deals with a Swing application rather than a web application.
src/test/java/se/waymark/rentit/steps/RentACarSupport.java
package se.waymark.rentit.steps; import org.fest.swing.edt.GuiActionRunner; import org.fest.swing.edt.GuiQuery; import org.fest.swing.fixture.*; import se.waymark.rentit.controller.MainController; import se.waymark.rentit.view.MainFrame; public class RentACarSupport { public void createCars(int availableCars) { FrameFixture window = getFrameFixture(); try { JMenuItemFixture addCars = window.menuItem("showAddCarsForm"); addCars.click(); JTextComponentFixture numberOfCars = window.textBox("numberOfCars"); numberOfCars.setText("" + availableCars); JButtonFixture createButton = window.button("createButton"); createButton.click(); } finally { window.cleanUp(); } } public void rentACar() { FrameFixture window = getFrameFixture(); try { JMenuItemFixture rentMenuItem = window.menuItem("rentMenuItem"); rentMenuItem.click(); } finally { window.cleanUp(); } } public int getAvailableNumberOfCars() { FrameFixture window = getFrameFixture(); try { JLabelFixture availableCarLabel = window.label("availableCarsValueLabel"); String availableCars = availableCarLabel.text(); return Integer.parseInt(availableCars); } finally { window.cleanUp(); } } private FrameFixture getFrameFixture() { MainFrame frame = GuiActionRunner.execute(new GuiQuery<MainFrame>() { protected MainFrame executeInEDT() { MainController controller = new MainController(); return new MainFrame(controller); } }); return new FrameFixture(frame); } }
Instead of using Selenium, I now use FEST for driving the GUI.
This was the interesting part from a testing point of view.
To execute this, we need a Maven pom that defines the project
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.waymark</groupId> <artifactId>swing-app</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>se.waymark</groupId> <artifactId>swing-test</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.12</version> <executions> <execution> <id>integration-test</id> <phase>integration-test</phase> <goals> <goal>integration-test</goal> </goals> </execution> <execution> <id>verify</id> <phase>verify</phase> <goals> <goal>verify</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>se.waymark</groupId> <artifactId>swing-main</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.easytesting</groupId> <artifactId>fest-swing</artifactId> <version>1.2.1</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.1.1</version> <scope>test</scope> </dependency> </dependencies> </project>
It is similar to the web application versions. It uses Maven fail-safe plugin to execute the test. But it doesn't use cargo to deploy the application on a Tomcat.
To be able to build the system, I need to implement it. This is a trivial Swing application and not really important but included here for completeness. I will therefore skip through it fast. All files needed are included, but I will not go through the details. There are a lot of other people out there who are better sent to describe a Swing application than I am.
All files needed for this example are organised like this:
swing-app |-- pom.xml |-- swing-main | |-- pom.xml | `-- src | `-- main | `-- java | `-- se | `-- waymark | `-- rentit | |-- controller | | `-- MainController.java | |-- Main.java | `-- view | `-- MainFrame.java `-- swing-test |-- pom.xml `-- src `-- test |-- java | `-- se | `-- waymark | `-- rentit | |-- RunCukesIT.java | `-- steps | |-- RentACarSupport.java | `-- RentStepdefs.java `-- resources `-- se `-- waymark `-- rentit `-- Rent.feature
All test files has already been presented so I will not do that again. The files needed looks like this:
A parent pom is used to connect the two sub modules. It is defined as:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.waymark</groupId> <artifactId>swing-app</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>swing-main</module> <module>swing-test</module> </modules> </project>
swing-main/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.waymark</groupId> <artifactId>swing-app</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>se.waymark</groupId> <artifactId>swing-main</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>se.waymark.educational</groupId> <artifactId>model</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> </dependencies> </project>
Nothing interesting here. It builds a java application and creates a jar file for the distribution.
The starting point is a class I call Main that contains the main method for the application. It creates a view and a controller.
src/main/java/se/waymark/rentit/Main.java
package se.waymark.rentit; import se.waymark.rentit.controller.MainController; import se.waymark.rentit.view.MainFrame; public class Main { public static void main(String[] args) { MainController controller = new MainController(); new MainFrame(controller); } }
The view is implemented as a frame that creates the entire GUI. It holds a reference to a controller so the model can be reached.
src/main/java/se/waymark/rentit/view/MainFrame.java
package se.waymark.rentit.view; import se.waymark.rentit.controller.MainController; import javax.swing.*; import java.awt.*; import java.awt.event.ActionListener; public class MainFrame extends JFrame { private MainController controller; private JTextField numberOfCars; public MainFrame(MainController controller) { this.controller = controller; controller.addView(this); addMenu(controller); createMainFrame(); controller.showAvailableCars(); } public void showAvailableCars(int availableCars) { clearContentPanel(); JPanel contentPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); addAvailableCarsLeadingText(contentPanel); addAvailableCarsLabel(availableCars, contentPanel); add(contentPanel); } private void addAvailableCarsLeadingText(JPanel contentPanel) { JLabel availableCarsLabel = new JLabel(); availableCarsLabel.setText("Available compact cars: "); contentPanel.add(availableCarsLabel); } private void addAvailableCarsLabel(int availableCars, JPanel contentPanel) { JLabel availableCarsValueLabel = new JLabel(); availableCarsValueLabel.setName("availableCarsValueLabel"); availableCarsValueLabel.setText("" + availableCars); contentPanel.add(availableCarsValueLabel); } public void showCreateCars() { clearContentPanel(); JPanel contentPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); addNumberOfCarsLabel(contentPanel); addNumberOfCarsField(contentPanel); addCreateButton(contentPanel); add(contentPanel); } private void addNumberOfCarsLabel(JPanel contentPanel) { JLabel availableCarsLabel = new JLabel(); availableCarsLabel.setText("Number of cars: "); contentPanel.add(availableCarsLabel); } private void addNumberOfCarsField(JPanel contentPanel) { numberOfCars = new JTextField(" "); numberOfCars.setName("numberOfCars"); contentPanel.add(numberOfCars); } private void addCreateButton(JPanel contentPanel) { JButton createButton = new JButton("Create cars"); createButton.setName("createButton"); createButton.setActionCommand("createCars"); createButton.addActionListener(controller); contentPanel.add(createButton); } public Component add(JPanel component) { super.add(component); component.revalidate(); return component; } public JTextField getNumberOfCarsTextField() { return numberOfCars; } private void addMenu(ActionListener controller) { JMenuBar menu = new JMenuBar(); setMenuLayout(menu); addMenuItems(controller, menu); setJMenuBar(menu); } private void createMainFrame() { setSize(400, 600); setVisible(true); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } private void setMenuLayout(JMenuBar menu) { LayoutManager layoutManager = new FlowLayout(FlowLayout.LEFT); menu.setLayout(layoutManager); } private void addMenuItems(ActionListener controller, JMenuBar menu) { menu.add(getFileMenu(controller)); menu.add(getRentMenu(controller)); menu.add(getToolsMenu(controller)); } private JMenu getFileMenu(ActionListener controller) { JMenu file = new JMenu("File"); JMenuItem exit = new JMenuItem("Exit"); exit.setActionCommand("exit"); exit.addActionListener(controller); file.add(exit); return file; } private JMenuItem getRentMenu(ActionListener controller) { JMenuItem rent = new JMenuItem("Rent"); rent.setName("rentMenuItem"); rent.setActionCommand("rentCar"); rent.addActionListener(controller); return rent; } private JMenu getToolsMenu(ActionListener controller) { JMenu tools = new JMenu("Tools"); JMenuItem addCars = new JMenuItem("Add cars"); addCars.setName("showAddCarsForm"); addCars.setActionCommand("showAddCarsForm"); addCars.addActionListener(controller); tools.add(addCars); return tools; } private void clearContentPanel() { getContentPane().removeAll(); } }
The controller connects the view and model. It is implemented as:
src/main/java/se/waymark/rentit/controller/MainController.java
package se.waymark.rentit.controller; import se.waymark.rentit.model.dao.CarDAO; import se.waymark.rentit.model.dao.InMemoryCarDAO; import se.waymark.rentit.model.entiy.Car; import se.waymark.rentit.view.MainFrame; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class MainController implements ActionListener { private CarDAO carDAO = new InMemoryCarDAO(); private MainFrame view; @Override public void actionPerformed(ActionEvent actionEvent) { String actionCommand = actionEvent.getActionCommand(); if (actionCommand.equalsIgnoreCase("showAddCarsForm")) { showAddCarsForm(); } else if (actionCommand.equalsIgnoreCase("createCars")) { createCars(); } else if (actionCommand.equalsIgnoreCase("rentCar")) { rentCar(); } else if (actionCommand.equalsIgnoreCase("exit")) { exit(); } else { System.out.println("Unknown action command: " + actionCommand); } } private void showAddCarsForm() { view.showCreateCars(); } private void createCars() { JTextField textField = view.getNumberOfCarsTextField(); String carsToCreateString = textField.getText().trim(); int carsToCreate = Integer.parseInt(carsToCreateString); for (int i = 0; i < carsToCreate; i++) { Car car = new Car(); carDAO.add(car); } int numberOfAvailableCars = carDAO.getNumberOfAvailableCars(); view.showAvailableCars(numberOfAvailableCars); } public void showAvailableCars() { int numberOfAvailableCars; try { numberOfAvailableCars = carDAO.getNumberOfAvailableCars(); } catch (RuntimeException e) { numberOfAvailableCars = 0; } view.showAvailableCars(numberOfAvailableCars); } private void rentCar() { int numberOfAvailableCars; try { carDAO.findAvailableCar().rent(); numberOfAvailableCars = carDAO.getNumberOfAvailableCars(); } catch (RuntimeException e) { numberOfAvailableCars = 0; } view.showAvailableCars(numberOfAvailableCars); } private void exit() { System.exit(0); } public void addView(MainFrame view) { this.view = view; } }
A simple Swing application can be added on top of the model without changing the defined behaviour. This is Behaviour Driven Development. You define the behaviour you want. Implement it. Add a GUI if it is needed but don't change the behaviour if you don't have to. Next exercise is to use the same behaviour but without a GUI. I will use a RESTFul web service this time.