Building a Swing GUI

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.

The Swing application

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.

File organisation

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:

Parent pom

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>

Main application pom

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.

Application

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);
    }
}

View

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();
    }
}

Controller

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;
    }
}

Conclusion

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.

Next - A RESTFul web service



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Git
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS