Test Driven Development com Java Swing

Deixarei hoje algumas dicas sobre como desenvolver uma aplicação em Java Swing de conversão de temperaturas utilizando uma técnica de desenvolvimento de software conhecida como TDD (Test Driven Development). Então, se você nunca ouviu falar em TDD antes, procure dar uma estudada para saber do que se trata antes continuar lendo este texto. Garanto que, por mais esquisito que esse negócio de TDD possa parecer, é um jeito bem interessante de se desenvolver aplicações.

Basicamente, a sequência que seguiremos no decorrer deste artigo será a seguinte: primeiramente, criaremos testes de unidade com o JUnit para definir a lógica para conversão de uma temperatura para outras escalas (Celsius, Kelvin e Fahrenheit). Em seguida, iremos programar essa lógica até que todos os testes passem. Depois disso, a interface com o usuário será desenhada e o seu comportamento definido, através da criação de testes JUnit + FEST-Swing, ferramenta esta muito interessante que permite automatizar testes em aplicações Swing. Por fim, programaremos a lógica da View até que todos os testes passem.

É, eu sei… na teoria tudo parece muito bonito, mas, e na prática? Será que funciona assim mesmo? Se está curioso, então continue lendo!

Ferramentas e arquivos

Como IDE, utilizaremos o NetBeans 6.1 pela facilidade em se desenvolver aplicações desktop. Você também precisará baixar baixar a última distribuição do FEST-Swing e adicioná-la no seu NetBeans através do Library Manager.

Definindo a lógica de conversão de temperatura

Vamos pensar agora em como a lógica de conversão de temperatura deve funcionar. Para tornar mais fácil a tarefa de se obter uma arquitetura limpa e “coerente”, escrevemos os testes unitários antes do código que implementa a lógica de conversão.

Isso mesmo, primeiro os testes e depois a implementação. Se você tivesse lido um pouco sobre TDD, provavelmente não teria se espantado tanto! Abaixo, um trecho do livro Extreme Programming, de Vinícios Manhães Teles:

Quando o desenvolvedor pensa no teste antes de pensar na implementação, ele é forçado a compreender melhor o problema [...]. Ao se aprofundar no problema, o desenvolvedor está fazendo uma análise mais detalhada. Portanto, neste momento, o desenvolvimento guiado por testes (TDD) atua como uma técnica de análise [...].

Quando o desenvolvedor escreve o teste, ele procura atuar como um cliente dele, se preocupando apenas com a interface externa do método, sem dar atenção à implementação. Isso é ótimo porque permite que o design do método seja o mais adequado possível para aqueles que irão utilizá-lo. [...]

Isso faz sentido para você ou não? Para mim - e para dezenas de milhares de outros desenvolvedores - faz muito sentido!

Nem preciso dizer que este livro é obrigatório para aqueles que querem aprender, de uma vez por todas, as práticas do XP. Sei que não estou ganhando nada pela propaganda, mas é que o livro é muito bom mesmo!

Continuando, confira abaixo os testes que servirão, ao mesmo tempo, para definir o design do código de conversão e testá-lo:

package com.destaquenet.tutorial.tdd;
 
// imports ...
 
public class ConversorTemperaturaImplTest {
    private ConversorTemperatura conversion;
 
    @Before
    public void setUp() {
        conversion = new ConversorTemperaturaImpl();
    }
 
    @Test
    public void converterParaCelsius() {
        assertEquals(conversion.converter(15.0, Escala.CELSIUS, Escala.CELSIUS), 15.0);
        assertEquals(conversion.converter(50.0, Escala.FAHRENHEIT, Escala.CELSIUS), 10.0);
        assertEquals(conversion.converter(561.5, Escala.KELVIN, Escala.CELSIUS), 288.35);
    }
 
    @Test
    public void converterParaFahrenheit() {
        assertEquals(conversion.converter(15.0, Escala.CELSIUS, Escala.FAHRENHEIT), 59.0);
        assertEquals(conversion.converter(50.0, Escala.FAHRENHEIT, Escala.FAHRENHEIT), 50.0);
        assertEquals(conversion.converter(561.5, Escala.KELVIN, Escala.FAHRENHEIT), 551.03);
    }
 
    @Test
    public void converterParaKelvin() {
        assertEquals(conversion.converter(15.0, Escala.CELSIUS, Escala.KELVIN), 288.15);
        assertEquals(conversion.converter(50.0, Escala.FAHRENHEIT, Escala.KELVIN), 283.15);
        assertEquals(conversion.converter(561.5, Escala.KELVIN, Escala.KELVIN), 561.5);
    }
}

Como pôde ser visto, a classe de conversão de temperaturas deverá ser simples e eficiente para a situação proposta. Ela terá apenas um método converter() que receberá três parâmetros:

  • Temperatura a ser convertida;
  • Escala de origem (Celsius, Kelvin ou Fahrenheit); e
  • Escala de destino (Celsius, Kelvin ou Fahrenheit).

Neste momento o que temos é um código todo sublinhado em vermelho. Portanto, nossa tarefa agora é fazer com que eles chegem a pelo menos compilar.

Em primeiro lugar, crie o enum Escala:

package com.destaquenet.tutorial.tdd;
 
public enum Escala {
    CELSIUS, FAHRENHEIT, KELVIN
}

Depois, crie a interface ConversorTemperatura:

package com.destaquenet.tutorial.tdd;
 
public interface ConversorTemperatura {
    double converter(double valor, Escala de, Escala para);
}

Finalmente, crie a classe ConversorTemperaturaImpl:

package com.destaquenet.tutorial.tdd;
 
public class ConversorTemperaturaImpl implements ConversorTemperatura {
 
    @Override
    public double converter(double valor, Escala de, Escala para) {
        return 0.0;
    }
 
}

Neste ponto já temos um código compilado, embora o mesmo não passe nos testes que definimos anteriormente. A tarefa agora é ir alterando este código até que todos aqueles testes passem:

package com.destaquenet.tutorial.tdd;
 
public class ConversorTemperaturaImpl implements ConversorTemperatura {
 
    @Override
    public double converter(double valor, Escala de, Escala para) {
        double result = 0.0;
 
        /* conversão desnecessária */
        if (de == para) {
            return valor;
        }
 
        if (de == Escala.CELSIUS && para == Escala.FAHRENHEIT) {
            result = valor * 1.8 + 32;
        } else if (de == Escala.CELSIUS && para == Escala.KELVIN) {
            result = valor + 273.15;
        } else if (de == Escala.FAHRENHEIT && para == Escala.CELSIUS) {
            result = (valor - 32) / 1.8;
        } else if (de == Escala.FAHRENHEIT && para == Escala.KELVIN) {
            result = (valor + 459.67) / 1.8;
        } else if (de == Escala.KELVIN && para == Escala.CELSIUS) {
            result = valor - 273.15;
        } else if (de == Escala.KELVIN && para == Escala.FAHRENHEIT) {
            result = valor * 1.8 - 459.67;
        }
        return result;
    }
}

Ok, neste momento os testes já devem executar com sucesso, o que significa que terminamos a codificação da lógica de conversão de temperaturas. Podemos então partir para a definição da interface com o usuário.

Definindo a interface com o usuário

A melhor forma de saber como o usuário irá interagir com o sistema é criar uma representação da interface com a qual ele irá trabalhar. Isso pode ser feito de diversas formas: desde um rabisco à lápis em uma folha sulfite até através do uso de um software.

No NetBeans, essa tarefa é muito simples de ser feita, já que contamos com a ajuda do Matisse, o módulo de GUI design. Com ele, podemos criar interfaces gráficas complexas em minutos, o que torna esta ferramenta ideal para prototipação.

Ok, depois de alguns minutos mexendo no Matisse, o resultado que obtive pode ser visto na imagem abaixo:

Tela do aplicativo

Tela do aplicativo

A interação do usuário com essa tela é bem fácil de se entender. O usuário fornecerá a temperatura a ser convertida (através da digitação do valor e escolha da escala). Em seguida, ele pressionará o botão Converter. Obviamente, o sistema deverá informar o usuário caso ele forneça uma temperatura inválida. Caso a temperatura seja válida, a aplicação faz a conversão e mostra o resultado nas três caixas de texto na parte inferior da tela. Os botões, na parte inferior da tela, explicam suas funções por si só.

Definindo o comportamento da interface com o usuário

Antes de programar a interface com o usuário, vamos aplicar uma abordagem semelhante à aplicada para definir a lógica de conversão de temperaturas, ou seja, vamos criar os testes primeiro:

package com.destaquenet.tutorial.tdd;
 
// imports ...
 
public class ConversorFrameTest {
    private FrameFixture frame;
 
    @Before
    public void setUp() {
        frame = new FrameFixture(new ConversorFrame());
        frame.show();
    }
 
    @After
    public void tearDown() {
        frame.cleanUp();
    }
 
    @Test
    public void checarValoresIniciais() {
        frame.textBox("temperatura").requireText("0.0");
        frame.textBox("celsius").requireText("0.0");
        frame.textBox("fahrenheit").requireText("0.0");
        frame.textBox("kelvin").requireText("0.0");
        frame.comboBox().requireSelection(Escala.CELSIUS.toString());
    }
 
    @Test
    public void converterTemperaturaVazia() {
        frame.textBox("temperatura").deleteText();
        frame.comboBox().selectItem(Escala.FAHRENHEIT.toString());
        frame.button("converter").click();
        frame.optionPane().requireErrorMessage().okButton().click();
    }
 
    @Test
    public void converterTemperaturaInvalida() {
        frame.textBox("temperatura").deleteText().enterText("aaa");
        frame.button("converter").click();
        frame.optionPane().requireErrorMessage().okButton().click();
    }
 
    @Test
    public void converterDeFahrenheit() {
        frame.textBox("temperatura").deleteText().enterText("50.0");
        frame.comboBox().selectItem(Escala.FAHRENHEIT.toString());
        frame.button("converter").click();
        frame.textBox("celsius").requireText("10.0");
        frame.textBox("fahrenheit").requireText("50.0");
        frame.textBox("kelvin").requireText("283.15");
    }
 
    @Test
    public void converterDeKelvin() {
        frame.textBox("temperatura").deleteText().enterText("450.2");
        frame.comboBox().selectItem(Escala.KELVIN.toString());
        frame.button("converter").click();
        frame.textBox("celsius").requireText("177.05");
        frame.textBox("fahrenheit").requireText("350.69");
        frame.textBox("kelvin").requireText("450.2");
    }
 
    @Test
    public void converterDeCelsius() {
        frame.textBox("temperatura").deleteText().enterText("50.5");
        frame.comboBox().selectItem(Escala.CELSIUS.toString());
        frame.button("converter").click();
        frame.textBox("celsius").requireText("50.5");
        frame.textBox("fahrenheit").requireText("122.9");
        frame.textBox("kelvin").requireText("323.65");
    }
 
    @Test
    public void mostrarJanelaSobre() {
        frame.button("sobre").click();
        frame.optionPane().requireInformationMessage().okButton().click();
    }
 
    @Test
    public void fecharAplicativo() {
        frame.button("sair").click();
        assert !frame.component().isVisible();
    }
}

Uma das características mais marcantes do FEST-Swing é o uso de interfaces fluentes na API, tornando muito simples a criação dos testes antes mesmo de criarmos a interface com o usuário.

Agora vamos dar uma olhada rápida no código. Primeiramente, antes de cada teste ser executado, o método setUp() cria um objeto FrameFixture passando uma nova instância do Frame que desejamos testar. É através desse objeto fixture que podemos realizar asserções em elementos da interface gráfica.

No nosso caso, utilizamos uma instância de FrameFixture pois estamos testando um objeto Frame. Entretanto, existem diversos outros tipos de fixtures que permitem testar outros componentes (Dialog, JOptionPane etc).

Como os métodos do objeto fixture são basicamente instruções dadas a um usuário “de mentira”, fica muito fácil adivinhar o que acontece em cada um dos testes. Perceba que todas essas instruções seguem um padrão: informamos o componente desejado, disparamos ações nesse componente e verificamos a resposta do componente controlado pela fixture.

Finalmente, o método tearDown() libera os recursos utilizados por cada teste.

Ok, você pode executar os testes agora. Não se espante quando seu teclado e mouse ganharem “vida”… é o FEST-Swing usando sua aplicação!

Implementando a lógica da interface com o usuário

Os testes falharam! Por quê? Ora, porque ainda não implementamos a lógica de interação do usuário com a aplicação! Mas, fique tranquilo, pois é exatamente isto que iremos fazer agora.

Primeiramente, vamos configurar o Model do nosso JComboBox para que este mostre o nosso enum Escala:

new DefaultComboBoxModel(Escala.values())

Adicione um ActionListener no botão Converter com o seguinte código:

double temp = 0.0;
Escala scale = (Escala) escala.getSelectedItem();
ConversorTemperatura conversor = new ConversorTemperaturaImpl();
 
// Limpa os resultados anteriores
celsius.setText("0.0");
fahrenheit.setText("0.0");
kelvin.setText("0.0");
 
try {
    temp = Double.valueOf(temperatura.getText()); // Converte para double
 
    /* Mostra os resultados */
    celsius.setText("" + conversor.converter(temp, scale, Escala.CELSIUS));
    fahrenheit.setText("" + conversor.converter(temp, scale, Escala.FAHRENHEIT));
    kelvin.setText("" + conversor.converter(temp, scale, Escala.KELVIN));
} catch (Exception e) {
    /* Erro na obtenção da temperatura */
    JOptionPane.showMessageDialog(this, "Temperatura inválida", "Erro", JOptionPane.ERROR_MESSAGE);
    temperatura.requestFocus();
}

Adicione um ActionListener no botão Sobre… com o seguinte código:

JOptionPane.showMessageDialog(this,
    "TDD (Test Driven Development) e Swing?\nAutor: Daniel F. Martins",
    "Sobre...", JOptionPane.INFORMATION_MESSAGE);

E, finalmente, adicione um ActionListener (que novidade…) no botão Sair, com o seguinte código:

dispose();

Agora todos os testes devem passar, o que significa que a aplicação está pronta!

Conclusão

O propósito aqui não é explicar tudo sobre o TDD e suas vantagens e desvantagens, eu deixo essa tarefa para outros textos disponíveis por aí. O que eu tentei fazer aqui foi apenas ilustrar, ou simular, o desenvolvimento de uma aplicação Swing usando TDD, para provar que sim, é possível desenvolver aplicações Swing de um modo ágil.

Se você costuma ler sobre programação desktop, seja em Java ou em qualquer plataforma, você provavelmente já deve ter ouvido aquela máxima de que é “impossível testar interfaces com o usuário”. Com o FEST-Swing isso nunca foi tão… mentira. :)

Veja também:

Tags: , , , , , ,

Um comentário para “Test Driven Development com Java Swing”

  1. [...] testes automatizados (e até mesmo levá-los a outro nível, como acontece com o TDD) é essencial para que as aplicações evoluam de forma consistente. Sem a segurança que [...]

Deixe um comentário