
package com.bh.tdd;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestClass {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
}

Dollar 클래스를 작성하여, 문법적인 에러를 해결
package com.bh.tdd;
public class Dollar {
int amount;
public Dollar(int amount) {}
public void times(int multiplier){}
}

테스트를 통과하기 위한 코드로 변경
package com.bh.tdd;
public class Dollar {
int amount;
public Dollar(int amount) {
this.amount = amount;
}
public void times(int multiplier){
amount *= multiplier;
}
}

현재의 코드는 멱등성이 보장되지 않는다.
객체에 동일한 연산(times)을 여러 번 수행했을 때, 결과가 동일하지 않다.
public class TestClass {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
five.times(2);
assertEquals(10, five.amount);
}
}
테스트 실패

times 메서드가 멱등성을 보장하도록 Dollar 클래스 수정
package com.bh.tdd;
public class Dollar {
int amount;
public Dollar(int amount) {
this.amount = amount;
}
public Dollar times(int multiplier){
return new Dollar(amount * multiplier);
}
}
Dollar 클래스 변경에 맞추어, test 코드도 수정
package com.bh.tdd;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestClass {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
assertEquals(10, five.times(2).amount);
assertEquals(10, five.times(2).amount);
}
}
동일한 연산에 대해 동일한 결과를 만들어 낸다. 즉, 멱등성을 보장한다.

값 객체를 위한 클래스 수정
package com.bh.tdd;
public class Dollar {
private final int amount;
public Dollar(int amount){
this.amount = amount;
}
public Dollar times(int multiplier){
return new Dollar(amount * multiplier);
}
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
return this.amount == ((Dollar)o).amount;
}
}
테스트 클래스 수정
package com.bh.tdd;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestClass {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
}
@Test
public void testFrancMultiplication(){
Franc five = new Franc(5);
assertEquals(new Franc(10), five.times(2));
assertEquals(new Franc(15), five.times(3));
}테스트 코드가 오류를 일으키기 때문에, Franc 클래스를 만들고 구현
Dollar 클래스와 유사하기 때문에 복사한 후 수정해서 작성
package com.bh.tdd;
public class Franc {
private final int amount;
public Franc(int amount){
this.amount = amount;
}
public Franc times(int multiplier){
return new Franc(amount * multiplier);
}
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
return this.amount == ((Franc)o).amount;
}
}
amount 속성을 상위 클래스로 이동equals 메서드를 상위 클래스에 작성수정한 결과
상위 클래스에 해당하는 Money 클래스 : Money.java
public abstract class Money {
protected int amount;
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
return this.amount == ((Money)o).amount;
}
}
Dollar 클래스 : Dollar.java
public class Dollar extends Money {
public Dollar(int amount){
this.amount = amount;
}
public Dollar times(int multiplier){
return new Dollar(amount * multiplier);
}
}
Franc 클래스 : Franc.java
public class Franc extends Money {
public Franc(int amount){
this.amount = amount;
}
public Franc times(int multiplier){
return new Franc(amount * multiplier);
}
}
이전 작업에서 중복된 부분을 많이 생략했지만 아래 코드를 테스트에 추가하면 이상한 결과가 도출됨
@Test
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)));
//서로 다른 타입의 데이터는 비교 대상이 아닙니다.
//이런 경우는 에러가 발생하거나 false가 리턴되어야 하는데 amount 값이 같다는 이유로
//비교가 가능하고 동일하다고 리턴됩니다.
assertTrue(new Dollar(5).equals(new Franc(5)));
}
서로 다른 타입의 데이터 비교는 불가해야 하는데, 테스트가 성공했다..

Money 클래스를 수정
public class Money {
protected int amount;
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
Money money = (Money)o;
//값만 비교하지 않고 자료형도 비교
return getClass().equals(money.getClass()) && this.amount == money.amount ;
}
}
다시 테스트해보면, 5번째 비교에서 Dollar 와 Franc 이 다르다고 나온다.

@Test
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)));
//서로 다른 타입의 데이터를 비교했을 때는 false 가 리턴되어야 한다.
assertFalse(new Dollar(5).equals(new Franc(5)));
} 다시 테스트를 수행하면, 테스트에 통과한다.
테스트 코드에서는 하위 클래스의 변경에 관심을 가질 필요가 없음
테스트 클래스 내용 수정
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TestClass {
@Test
public void testMultiplication(){
//dollar 라는 메서드는 실제로는 Dollar 객체를 리턴하므로 five에 들어있는 객체는 Dollar 객체
//times는 Money 클래스에 추상메서드로 존재하므로 문법적 에러가 없어지고
//Dollar에서 오버라이딩을 했으므로 five가 호출할 때는 Dollar에 만든 메서드가 호출됩니다.
Money five = Money.dollar(5);
assertEquals(Money.dollar(10), five.times(2));
assertEquals(Money.dollar(15), five.times(3));
}
@Test
public void testFrancMultiplication(){
Money five = Money.franc(5);
assertEquals(Money.franc(10), five.times(2));
assertEquals(Money.franc(15), five.times(3));
}
@Test
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)));
//서로 다른 타입의 데이터를 비교했을 때는 false 가 리턴되어야 한다.
assertFalse(Money.dollar(5).equals(Money.franc(5)));
}
}
상위 클래스 내용 수정: 문법적인 오류만 수정
package com.bh.tdd;
public abstract class Money {
protected int amount;
static Dollar dollar(int amount){
return new Dollar(amount);
}
static Franc franc(int amount){
return new Franc(amount);
}
abstract Money times(int multiplier);
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
Money money = (Money)o;
//값만 비교하지 않고 자료형도 비교
return getClass().equals(money.getClass()) && this.amount == money.amount ;
}
}
구현 내용
동일한 모양의 메서드의 선언부를 통일시켜서 중복 제거를 위해서 전진
메서드 구현부는 어쩔 수 없지만 선언부를 통일시켜서 앞으로 추가될 기능에 템플릿을 제공
팩토리 메서드를 만들어서 테스트 코드에서 콘크리트 하위 클래스의 존재 사실을 분리해냄
추상 클래스가 만들어져 있는 상황에서는 추상 클래스에 기능의 원형을 추가하고 실제 사용하는 콘크리트 클래스에 기능을 구현하는 방식으로 기능을 추가합니다.
Dollar 와 Franc 클래스에 통화 기호를 리턴하는 기능을 추가하고자 하는 경우
@Test
public void testCurrency(){
assertEquals("USD", Money.dollar(1).currency());
assertEquals("CHF", Money.franc(1).currency());
}abstract String currency();테스트를 해보면, 문법적으로는 문제가 없지만 abstract 메서드에 대한 구현이 없어서 에러
String currency() {
return "USD";
}String currency() {
return "CHF";
}
Money 클래스
package com.bh.tdd;
public abstract class Money {
protected int amount;
protected String currency;
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
static Dollar dollar(int amount) {
return new Dollar(amount, "USD");
}
static Franc franc(int amount) {
return new Franc(amount, "CHF");
}
abstract Money times(int multiplier);
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
Money money = (Money)o;
//값만 비교하지 않고 자료형도 비교
return getClass().equals(money.getClass()) && this.amount == money.amount ;
}
String currency() {
return currency;
}
}
Dollar 클래스
public class Dollar extends Money{
public Dollar(int amount, String currency) {
super(amount, currency);
}
public Money times(int multiplier){
return Money.dollar(amount * multiplier);
}
}
Franc 클래스
public class Franc extends Money{
public Franc(int amount, String currency) {
super(amount, currency);
}
public Money times(int multiplier){
return Money.franc(amount * multiplier);
}
}
Money 클래스 수정
public class Money {
protected int amount;
protected String currency;
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
static Dollar dollar(int amount){
return new Dollar(amount, "USD");
}
static Franc franc(int amount){
return new Franc(amount, "CHF");
}
public Money times(int multiplier){
return new Money(amount * multiplier, currency);
}
String currency() {
return currency;
}
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
Money money = (Money)o;
//값만 비교하지 않고 자료형도 비교
return currency.equals(money.currency()) && this.amount == money.amount ;
}
}
Dollar 클래스
public class Dollar extends Money{
public Dollar(int amount, String currency) {
super(amount, currency);
}
}
public class Franc extends Money{
public Franc(int amount, String currency) {
super(amount, currency);
}
}Money 클래스를 수정
package com.bh.tdd;
public class Money {
protected int amount;
protected String currency;
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
static Money dollar(int amount) {
return new Money(amount, "USD");
}
static Money franc(int amount) {
return new Money(amount, "CHF");
}
public Money times(int multiplier){
return new Money(amount * multiplier, currency);
}
public boolean equals(Object o){
//amount 값이 같으면 같은 걸로 간주
Money money = (Money)o;
//값만 비교하지 않고 자료형도 비교
return currency.equals(money.currency) && this.amount == money.amount ;
}
String currency() {
return currency;
}
}

테스트 코드에 실패하는 모양을 생성
@Test
public void testAddition(){
Money sum = Money.dollar(5).plus(Money.dollar(10));
assertEquals(Money.dollar(15), sum);
sum = Money.franc(5).plus(Money.franc(10));
assertEquals(Money.franc(15), sum);
}
에러가 없는 코드를 생성
public Money plus(Money money) {
return new Money(15, currency);
}
public Money plus(Money money) {
return new Money(amount + money.amount, currency);
}Service 계층의 단위 테스트를 할 때 이 과정이 적용
Repository 계층의 단위 테스트는 SQL을 호출해서 그 결과를 만들어내는 코드만 존재하기 때문에 단위 테스트를 수행할 때 작업의 결과만 확인
만들어진 메서드를 호출하는 경우가 대부분이라서 코드 리팩토링은 거의 존재하지 않습니다.