Prof. Marco Tulio Valente
Objetivo: reproduzir uma sessão de desenvolvimento de software usando TDD (Test-Driven Development).
Para isso, usaremos o mesmo exemplo do livro clássico de TDD, do Kent Beck. Especificamente, iremos reproduzir o exemplo do Cap. 1 até o Cap. 11 do livro (os capítulos são pequenos, com 3-4 páginas).
Faça o exercício com o modelo mental de TDD. Ou seja, para tirar proveito do exercício, é importante não apenas seguir o roteiro mecanicamente, mas também pensar sempre sobre as mudanças de fluxo de trabalho que ocorrem com TDD, os benefícios e desafios dessa técnica de escrita de código e testes.
Se necessário, estude antes o material visto em sala de aula. Caso tenha dúvidas, você pode consultar também o nosso livro, que explica os passos com mais detalhes.
O exemplo está em Java, mas a sintaxe é familiar mesmo para aqueles que nunca programaram na linguagem. Infelizmente, não é possível fazer em uma outra linguagem, pois a correção será automática. Você pode usar uma IDE Java (Eclipse, NetBeans, IntelliJ) ou então uma IDE online (como repl.it). Além disso, a próxima aula prática também será em Java.
Instruções:
-
Primeiro, crie um repositório no GitHub.
-
Vá seguindo o roteiro, passo a passo. Para facilitar, os passos são itemizadaos da seguinte forma: (1): Vermelho, (2): Verde, (3): Refactor. Isto é, exatamente os mesmos estados de desenvolvimento baseado em TDD.
-
Após cada passo, compile e rode os testes.
-
Nos passos marcados com COMMIT & PUSH, dê um commit e um push no seu repositório. Isso será usado no momento da correção, para garantir que seguiu a sequência do roteiro, passo-a-passo.
-
Importante: quando terminar, responda à seguinte issue, informando a URL do seu repositório e seu nome (caso contrário, seu trabalho não será corrigido).
- (1) Seja a seguinte classe e seu teste (exatamente o teste e a classe vistos em sala de aula, no final dos slides de TDD):
class Dollar {
int amount = 10;
Dollar(int amount) {}
void times(int multiplier) {}
}
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
-
Faça um COMMIT & PUSH
-
(2) Dando uma implementação mais real para a classe
Dollar
:
class Dollar {
int amount;
Dollar(int amount) {
this.amount= amount;
}
void times(int multiplier) {
amount= amount * multiplier;
}
}
- (1.1) Novo teste/feature: tornando
Dollar
imutável, i.e., agora, uma operação comotimes
retorna um novo objeto e não modifica o objecto corrente:
public void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10, product.amount);
product = five.times(3);
assertEquals(new Dollar(15), product);
}
Precisamos alterar Dolar.times()
para compilar o novo teste:
class Dollar {
...
Dollar times(int multiplier) {
return new Dollar(amount * multiplier);
}
}
- (1.2) Apenas um refactoring (no teste), para ele ficar menor:
public void testMultiplication() {
Dollar five = new Dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
- (1.2.1)
Dollar
é a única classe usando a variávelamount
após as mudanças no teste. Logo, podemos alterar o seu modificador de acesso paraprivate
:
class Dollar {
private int amount;
...
}
- (1.3) Novo teste/feature: método
equals
, para testar se doisDollar
são iguais.
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
}
- (2.1) Implementando
equals
(método da classeDollar
):
public boolean equals(Object object) {
Dollar dollar = (Dollar) object;
return amount == dollar.amount;
}
- (1) e (2): Novo teste/feature: classe
Franc
(além de dólar, agora temos uma classe para armazenar francos suíços);Franc
é basicamente uma cópia de Dollar, com os devidos ajustes de nomes.
public void testFrancMultiplication() {
Franc five = new Franc(5);
assertEquals(new Franc(10), five.times(2));
assertEquals(new Franc(15), five.times(3));
}
class Franc {
private int amount;
Franc(int amount) {
this.amount= amount;
}
Franc times(int multiplier) {
return new Franc(amount * multiplier);
}
public boolean equals(Object object) {
Franc franc = (Franc) object;
return amount == franc.amount;
}
}
- (3.1) Refactor: vamos criar uma superclasse
Money
; subiramount
para ela (comoprotected
) e também subirequals
(com alguns ajustes no código). Ou seja, ambos vão ser deletados das subclassesDollar
eFranc
e movidos paraMoney
. Depois, não se esqueça de fazerDollar
eFranc
herdarem deMoney
.
class Money {
protected int amount;
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount;
}
}
- (3.2) Mais alguns testes, que vão passar:
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));
}
-
Faça um COMMIT & PUSH
-
(1) Melhorando
testEquality
(só adicionando últimoassert
, no código abaixo), que comparaFranc
comDollar
.
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));
assertFalse(new Franc(5).equals(new Dollar(5)));
}
- (2) Agora
equals
verifica se as classes dos objetos comparados são iguais.
class Money {
...
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount && getClass().equals(money.getClass());
}
}
- (3) Refactoring:
times
agora retornaMoney
(eraDollar
eFranc
). Basicamente, estamos preparando o terreno para daqui a pouco remover essas classes. O código delas é muito parecido; e o objetivo do passo de Refactor em TDD é principalmente remover duplicação.
class Dollar {
...
Money times(int multiplier) {
return new Dollar(amount * multiplier);
}
}
class Franc {
...
Money times(int multiplier) {
return new Franc(amount * multiplier);
}
}
- (1) e (2) Novo teste/feature - método fábrica, chamado
Money.dollar
; mais uma passo para começar a eliminar a classeDollar
.
public void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(Money.dollar(10), five.times(2));
assertEquals(Money.dollar(15), five.times(3));
}
Adicionalmente, veja que tivemos que tornar Money
abstrata, para compilar (com o método times
abstrato):
abstract class Money {
...
static Dollar dollar(int amount) {
return new Dollar(amount);
}
abstract Money times(int multiplier);
}
- (1) e (2): Novo teste/feature -
Money.franc
(fábrica de Francos).
public void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6)));
assertTrue(Money.franc(5).equals(Money.franc(5)));
assertFalse(Money.franc(5).equals(Money.franc(6)));
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}
public void testFrancMultiplication() {
Money five = Money.franc(5);
assertEquals(Money.franc(10), five.times(2));
assertEquals(Money.franc(15), five.times(3));
}
class Money {
...
static Money dollar(int amount) {
return new Dollar(amount);
}
static Money franc(int amount) {
return new Franc(amount);
}
}
-
Faça um COMMIT & PUSH
-
(1) e (2): Novo teste/feature:
currency()
.
public void testCurrency() {
assertEquals("USD", Money.dollar(1).currency());
assertEquals("CHF", Money.franc(1).currency());
}
abstract class Money {
...
abstract String currency();
}
class Franc extends Money {
...
String currency() {
return "CHF";
}
}
class Dollar extends Money {
...
String currency() {
return "USD";
}
}
- (3.1): Refactor: adicionando um atributo
currency
, emFranc
eDollar
.
class Franc extends Money {
private String currency;
Franc(int amount) {
this.amount = amount;
currency = "CHF";
}
String currency() {
return currency;
}
}
class Dollar extends Money {
private String currency;
Dollar(int amount) {
this.amount = amount;
currency = "USD";
}
String currency() {
return currency;
}
}
- (3.2) Refactoring: subir
currency
paraMoney
(comoprotected
).
abstract class Money {
...
protected String currency;
...
String currency() {
return currency;
}
}
class Franc extends Money {
Franc(int amount) {
this.amount = amount;
currency = "CHF";
}
...
}
class Dollar extends Money {
Dollar(int amount) {
this.amount = amount;
currency = "USD";
}
...
}
- (3.3.) Refactor:
times
agora retornaMoney
, tanto emDollar
, como emFranc
.
class Dollar {
...
Money times(int multiplier) {
return Money.dollar(amount * multiplier);
}
}
class Franc {
...
Money times(int multiplier) {
return Money.franc(amount * multiplier);
}
}
- (3.4) Refactor: construtores ganham um parâmetro
currency
.
abstract class Money {
...
static Money dollar(int amount) {
return new Dollar(amount, "USD");
}
static Money franc(int amount) {
return new Franc(amount, "CHF");
}
}
class Franc extends Money {
Franc(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
}
class Dollar extends Money {
Dollar(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
}
- (3.5) Subir construtores para
Money
:
abstract class Money {
private String currency;
static Money dollar(int amount) {
return new Dollar(amount, "USD");
}
static Money franc(int amount) {
return new Franc(amount, "CHF");
}
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
}
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}
Money times(int multiplier) {
return Money.franc(amount * multiplier);
}
}
class Dollar extends Money {
Dollar(int amount, String currency) {
super(amount, currency);
}
Money times(int multiplier) {
return Money.dollar(amount * multiplier);
}
}
-
Faça um COMMIT & PUSH
-
(1) e (2) Novo teste/feature: comparando
Money
eFranc
.
public void testDifferentClassEquality() {
assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
}
A implementação inclui ainda: tornar Money
uma classe concreta (remover abstract
);
modificar o equals
(com isso, moedas de classes diferente
podem ser iguais, basta que amount
e currency
sejam iguais;
subir times
, com alguns ajustes, para Money
.
class Money {
...
static Money dollar(int amount) {
return new Dollar(amount, "USD");
}
static Money franc(int amount) {
return new Franc(amount, "CHF");
}
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount && currency().equals(money.currency());
}
Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}
}
- (3) Refactor: Alterar o seguinte teste:
public void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6)));
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}
Remover classes Dollar
e Franc
. Alterar as referências na classe Money
:
class Money {
static Money dollar(int amount) {
return new Money(amount, "USD");
}
static Money franc(int amount) {
return new Money(amount, "CHF");
}
...
}
E remover testDifferentClassEquality()
e testFrancMultiplication()
.
- Faça um COMMIT & PUSH