본문 바로가기

개발(합니다)/방법론

TDD 학습 및 실습 정리1(TDD의 기본사용법)

반응형

테스트 주도 개발 TDD 실천법과 도구 책을 참고했습니다.

바로 얍! 하고 하는 방법보다 테스트 주도 개발의 의의와 순서를 음미하고 싶어서 정리했습니다.

참고



TDD란

1. TDD를 주도한 켄트 벡트벡이 말하길 "프로그램을 작성하기 전에 테스트를 먼저 작성할 것"이라는 의미는 

"업무 코드를 작성하기 전에 테스트 코드를 먼저 만드는 것"입니다.

2. 최초에는 테스트 우선 개발(Test First Development)이었으나 테스트 주도 개발(Test Driven Development)로      불리고 있습니다.

3. 어떤 테크닉이나 테스트 프레임워크를 쓰지 않아도 TDD를 할 수 있습니다.


최대한 빨리 실패하고 자주 실패를 경험하는 방식입니다.

빨리 실패하면 실패 할수록 좀 더 성공에 가까워지는 묘한 개발 방식입니다.

TDD의 목표

잘 작동하는 깔끔한 코드


TDD의 기원

애자일 개발 방식 중 하나인 XP의 실천 방식 하나

TDD의 단위 테스트 단위

메소드 단위의 테스트

기존 개발 진행 방식

기능을 구현하고 테스트를 수행

TDD의 진행 방식

테스트 구현 후 기능 구현을 수행
구현 하는 메소드나 기능을 선별하고 작성 완료 조건을 정해서 실패하는 테스트 케이스를 작성하는것입니다.

클래스의 속성보다 클래스의 동작에 집중해야합니다.
속성을 어떤걸 넣어야 하는지 고민하기 보다 어떤 동작이 필요하니까 어떤 속성이 들어가야겠구나라고 생각해야합니다.
예를 들어 자동차라면 운전이라는 동작을 위해서는 핸들이 있어야 하고 엑셀이 있어야 하고 기어가 있어야 하고 사람과 자동차 버튼 or 키가 있어야 합니다. 
이런식으로 정지해야 할 때 필요한 걸 속성을 생각하면 됩니다.



1. 질문 : 테스트 작성을 통해 시스템에 질문합니다.(테스트 수행 결과는 실패)
2. 응답 : 테스트를 통과하는 코드를 작성해서 질문에 대답합니다.(테스트 성공)
3. 정제 : 아이디어를 통합하고 불필요한 것은 제거하고, 모호한 것은 명확히 해서 대답을 정제합니다.(리팩토링)
4. 반복 : 다음 질문을 통해 대화를 계속 진행합니다.



TDD를 이용한 개발은 '질문 -> 응답 -> 정제' 라는 세 단계가 반복적으로 이루어집니다.




실습 시작하기1

- 은행 계좌 클래스 만들기

질문, 응답, 정제 3 단계별로 작성했습니다.

질문 단계

어떤 기능을 구현할 것인지 ToDo 리스트로 작성하고 시스템에게 질문합니다.
"이 테스트 코드가 정상적으로 작동되는지 봐줄래?" 라고 물어볼뿐입니다.

ToDo 리스트
- 클래스 명칭 Account
- 기능 3가지
1. 잔고 조회
2. 입금
3. 출금
설명 : 금액은 원 단위로(천원 = 1000)

테스트 케이스 작성 하는 방법

1. 구현 대상 클래스의 외형에 해당하는 메소드들을 먼저 만들고 테스트 케이스를 일괄적으로 만드는 방식

2. 테스트 케이스를 하나씩 추가해나가면서 구현 클래스를 점진적으로 만드는 방식


보통 2번을 권장합니다.


1. AccountTest 클래스 생성를 생성합니다.

패키지명은 test로 해서 main 코드와 구분합니다.


2. 계좌를 생성합니다.

Account를 객체로 선언합니다. 
Account는 생성 하지 않았으니 에러가 나는게 당연합니다.
package annotation_test.account;

public class AccountTest {
    
    public void testAccount() {
        Account account = new Account();
        
    }
}
이런 식으로 작성하는 이유는 테스트 케이스 작성 시 흐름을 잃지 않기 위서입니다.



3. 정상적으로 생성 되었는지 확인합니다.

package annotation_test.account;

public class AccountTest {
    
    public void testAccount() {
        Account account = new Account();
        if(account == null) {
            throw new Exception("계좌 생성 실패");
        }
    }
}


4. main을 작성하고 테스트 합니다.

만일 testAccount() 메소드를 실행 했을 때 어떤 문제나 메세지가 발생하지 않는다면, 테스트를 성공으로 간주합니다.
하지만 지금은 당연히 에러가 발생합니다.

발생 한 에러에 짜증과 당황하지 않고 생성자에 대한 테스트 코드라고 생각해봅니다.

단지 시스템에게 개발자가 "코드가 예상대로 동작하는지 판단해줄래?" 라고 물어봤다고 생각해 봅니다.

시스템은 "아니!? 실패인데!?"라고 대답했을 뿐입니다.




응답 단계

시스템에게 질문의 응답을 받았습니다.
테스트를 성공 시켜봅니다.

1. Account 클래스를 생성합니다.

패키지를 main으로 변경하여 test 코드와 구분합니다.


2. 성공하는 코드로 변경합니다.

package annotation_test.test;

import annotation_test.main.Account;

public class AccountTest {
    
    public void testAccount() throws Exception{
        Account account = new Account();
        if(account == null) {
            throw new Exception("계좌 생성 실패");
        }
        
    }
    public static void main(String args[]) {
        AccountTest test = new AccountTest();
        
        try {
            test.testAccount();
        } catch (Exception e) {
            System.out.println("실패(X)");
            return ;
        }
        System.out.println("성공");
    }
    
}



정제 단계

- ToDo 목록에서 완료 된 부분을 지웁니다.
- 리팩토링을 적용할 부분이 있는지 찾아봅니다.
리팩토링은 정상적으로 동작하는 코드를 수정해서 '사람'이 이해하기 쉽고 변경이 용이한 구조로 소스코드를 개선하는 작업입니다.

1. 최초의 정제

- 소스의 가독성이 절적한가?
- 중복 된 코드는 없는가?
- 이름이 잘못 부여 된 메소드나 변수명은 없는가?
- 구조의 개선이 필요한 부분은 없는가?

ToDo 리스트
- 클래스 명칭 Account
- 기능 3가지
1. 잔고 조회
2. 입금
3. 출금
설명 : 금액은 원 단위로(천원 = 1000)

ToDo 리스트를 지워나가면서 1-3단계를 반복해서 수행합니다.

if문은 예외가 발생하지 않는 부분입니다.
정제 과정의 리팩토링하기 위해 if문을 제거합니다.
public void testAccount() throws Exception{
Account account = new Account();
if(account == null) {
throw new Exception("계좌 생성 실패");
}
}



package annotation_test.test;

import annotation_test.main.Account;

public class AccountTest {
    
    public void testAccount() throws Exception{
        Account account = new Account();
    }
    public static void main(String args[]) {
        AccountTest test = new AccountTest();
        
        try {
            test.testAccount();
            test.testGetBalance();
            ...
        } catch (Exception e) {
            System.out.println("실패(X)");
            return ;
        }
        System.out.println("성공");
    }
    
}


기본적인 TDD에 대한 이해 실습은 여기까지입니다.




JUnit을 이용한 테스트

TDD의 부흥을 만들고 소프트웨어 개발 방식의 전환을 만든 초석이 된 단위 테스트 프레임워크입니다.

1. 단위 테스트 프레임워크
2. 문자 혹은 GUI 기반으로 실행
3. xUnit이라 블리는 스타일을 따름
assertEquals(예상 값, 실제 값)
4. 결과는 성공(녹색), 실패(붉은색) 중 하나로 표시

- @Test를 사용해 봅니다.

라이브러리를 추가합니다.


라리브러리가 추가됩니다.


Junit을 실행합니다.


성공!!





실습 시작하기2

- 앞서 만든 은행 계좌 실습에서 잔고 조회 기능을 구현합니다.
- 테스트 실패 시 메세지를 보여 줄 수 있는 구조를 생각해봅니다.



질문 단계


작성 순서는 AccountTest.java를 작성하고 Account를 작성합니다.

AccountTest.java 
    @Test
    public void testAccount() throws Exception{
        Account account = new Account();
    }
    @Test
    public void testGetBalance() throws Exception{
        Account account = new Account(1000);
        if(account.getBalance()!= 1000) {
            fail();
        }
    }


Account.java
package src_test.main;
public class Account {
    public Account(int i) {
    }
    public int getBalance() {
        return 0;
    }
}

전부 작성하고 나면 testAccount()의 Account account = new Account()에서 에러가 납니다.
이때 예치금 없이 생성 하게 할 것인지 예치금이 있어야만 생성 할 수 있는지를 고민하는 계기가 됩니다.

예치금이 있어야만 생성 할 수 있도록 하겠습니다.
    @Test
    public void testAccount() throws Exception{
        Account account = new Account(1000);
    }

메인을 작성하고 실행합니다.
    public static void main(String args[]) {
        AccountTest test = new AccountTest();
        try {
            test.testAccount();
            test.testGetBalance();
        } catch (Exception e) {
            System.out.println("실패(X)");
            return ;
        }
        System.out.println("성공");
    }








오류와 실패의 차이


실패(Failures)는 AssertEquals 등의테스트 조건식을 만족시키지 못했다는 의미입니다.

오류(Errors)는 테스트 케이스 수행 중 예상치 못한 예외가 발생해서 테스트 수행을 멈췄다는 의미입니다


응답 단계

응답에 대한 처리를 합니다.


1. getBalance 메소드 호출 시 1000을 돌려주는 하드코딩을 합니다.

public int getBalance() {
        return 1000;
    }

성공입니다.


하드코딩하는 이유는 '첫 번째 녹색 막대는 신뢰하지 않기'로 하고 두 번째 테스트 케이스를 작성에 들어가기 위함입니다.

하드 코딩 값, 혹은 엉성하게 작성 된 테스트 케이스를 모두 통과하면, 찜찜하고 허탈 할 수 있지만 현재 가장 유력한 정답이 됩니다.

이렇게 만든 코드는 정제 단계에서 리팩토링하여 찜찜한 마음을 달랠 수 있습니다.


2. getBalance() 내용을 추가합니다.

    @Test
    public void testGetBalance() throws Exception{
        Account account = new Account(1000);
        if(account.getBalance( )!= 1000) {
            fail();
        }
        account = new Account(1000);
        if(account.getBalance() != 1000) {
            fail();
        }
        account = new Account(0);
        if(account.getBalance() != 0) {
            fail();
        }
            
    }



package src_test.main;

public class Account {
    private int balance;
    
    public Account(int i) {
        this.balance = i;
    }

    public int getBalance() {
        return this.balance;
    }
}


이번에도 작성 순서는 테스트 작성 후 메인을 작성합니다.



정제 단계

- 구현된 잔고 조회 로직에 대한 리팩토링 작업을 합니다.
- 본격적으로 JUnit 테스트 프레임워크를 사용합니다.


package src_test.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Test;

import src_test.main.Account;

public class AccountTest {
    
    @Test
    public void testAccount() throws Exception{
        Account account = new Account(1000);
    }
    @Test
    public void testGetBalance() throws Exception{
        Account account = new Account(10000);
        assertEquals("10000원으로 계좌 생성 후 잔고 조회", 10000, account.getBalance());
        
        
        account = new Account(1000);
        assertEquals("1000원으로 계좌 생성 후 잔고 조회", 1000, account.getBalance());
        
        account = new Account(0);
        assertEquals("0원으로 계좌 생성 후 잔고 조회", 0,account.getBalance());
            
    }
    
    public static void main(String args[]) {
        AccountTest test = new AccountTest();
        try {
            test.testAccount();
            test.testGetBalance();
        } catch (Exception e) {
            System.out.println("실패(X)");
            return ;
        }
        System.out.println("성공");
    }
    
}


package src_test.main;

public class Account {
    private int balance;
    
    public Account(int monery) {
        this.balance = monery;
    }

    public int getBalance() {
        return this.balance;
    }

}





실습 시작하기3

입금과 출금 테스트 

설명 없이 소스 첨부합니다.

1. 입금과 출금을 테스트 작성합니다.
2. 입금과 출금을 메인에서 작성합니다.
3. 변수 위치, 변수명 등 리펙토링합니다.



package src_test.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Test;

import src_test.main.Account;

public class AccountTest {
    
    private Account account;
    private Account account2;
    private Account account3;
    private Account account4;

    @Test
    public void testAccount() throws Exception{
        setup();
    }
    public void setup() {
        account = new Account(1000);
    }
    @Test
    public void testGetBalance() throws Exception{
        account2 = new Account(10000);
        assertEquals("10000원으로 계좌 생성 후 잔고 조회", 10000, account2.getBalance());
        
        setup();
        assertEquals("1000원으로 계좌 생성 후 잔고 조회", 1000, account.getBalance());
        
        account2 = new Account(0);
        assertEquals("0원으로 계좌 생성 후 잔고 조회", 0,account2.getBalance());
    }
    
    @Test
    public void testDoposit() throws Exception{
        account3 = new Account(1000);
        account3.deposit(1000);
        assertEquals("1000원 게좌에 1000원을 입금", 2000, account3.getBalance());
    }
    
    @Test
    public void testWithdraw() throws Exception{
        account4 = new Account(1000);
        account4.withraw(1000);
        assertEquals("1000원 계좌에 1000원을 출금", 0, account4.getBalance());
    }
    
    public static void main(String args[]) {
        AccountTest test = new AccountTest();
        try {
            test.testAccount();
            test.testGetBalance();
        } catch (Exception e) {
            System.out.println("실패(X)");
            return ;
        }
        System.out.println("성공");
    }
    
}


package src_test.main;

public class Account {
    private int balance;
    
    public Account(int monery) {
        this.balance = monery;
    }

    public int getBalance() {
        return this.balance;
    }

    public void deposit(int money) {
        this.balance += money;
    }

    public void withraw(int monery) {
        this.balance -= monery;
        
    }

}

반응형