[SpringBoot] TDD를 도입해보자

이동욱님의 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 책과 채수원님의 'TDD 실천법과 도구'를 참고했다.

코살람 유지보수를 다시 시작해볼까한다.

유지보수 때는 TDD 개발 방식을 도입해보려고 한다.
인턴 기간 중 테스트코드가 오히려 총 개발 시간을 줄여준다는 걸 깨달은 적이 있었다. 신뢰성은 덤~!
마침 코살람 유지보수를 시작하기로 마음먹었으니 여기에 적용해보려고 한다!

적용하고 나면 코드를 수정할 때마다 Tomcat 을 내렸다 켰다 하는 경험을 하지 않을 수 있게 될 것이다. 얏호


✏️ TDD란?

Test Driven Development의 약자. 테스트가 주도하는 개발

1. 항상 실패하는 테스트를 먼저 작성하고(RED)
2. 테스트를 통과하는 프로덕션 코드를 작성하고(Green)
3. 테스트가 통과하면 프로덕현 코드를 리팩토링한다.(Refactor) 

조금 더 자세한 설명

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

음 TDD는 테스트 작성을 먼저 한다는 게 가장 중요한 전제 조건인데
코살람은 이미 기능 구현이 돼있다.
TDD를 적용한다기보단 공부해서 테스트 코드를 추가했다...라고 봐야할듯하다.
물론 앞으로 추가되는 기능들은 전부 TDD를 적용할 예정이다.

Unit Test

  • 전통적인 테스트 방법론에서 말하는 Unit Test
    사용자 측면에서 제품의 기능(function)을 테스트
  • TDD에서 말하는 Unit test
    메소드 or 함수 단위의 테스트를 뜻함
  • 장점
    • 초기에 문제 발견 가능
    • 회귀 테스트 가능(코드 수정 / 리팩토링 시 기존 기능이 올바르게 작동하는지 확인 가능)
    • 기능에 대한 불확실성 감소
    • 시스템 문서 기능 역할을 수행할 수 있음

✏️ 사이클 예시

TDD 사이클을 한번 돌아보자!
JUnit 과 같은 단위테스트 프레임워크를 사용하지 않은 예시다.
역시 맨땅부터 찬찬히 공부하는 게 가장 효과적인 방법인 것 같다.

Ask 단계

테스트 작성으르 통해 시스템에 질문(결과는 실패로)

  • 테스트 시나리오 작성 : 기도실을 생성한다 -> 정상적으로 생성됐는지 확인한다.
  • 실패하는 테스트 메소드 생성

TDD에는 테스트의 최소 작성 단위를 최하위 모듈의 단위와 일치시킨다. Java 언어 기준으로 최하위 모듈은 method다.

첫 ask 단계에서는 메소드 수준의 단위 테스트를 작성한다.
작성하고자 하는 메소드나 기능이 무엇인지 선별하고
작성 완료 조건을 정해서 실패하는 테스트 케이스를 작성하는 것이다.

우선 구현해야하는 기능을 정리해보자

- 클래스 이름은 Prayerroom
- 기능
    - CRUD
    - 반경 Nkm 내의 기도실 리스트 조회
    - 좋아요
    - 리뷰

정리한 기능에 대한 테스트 케이스를 하나씩 추가해나가면서 구현 클래스를 점진적으로 만들어나가는 방식이 가장 좋다.

대부분의 경우 하나의 테스트 케이스는 하나의 메소드로 표현된다.

public class PrayerroomTest {
    public void testPrayerroom() {
        Prayerroom prayerroom = new Prayerroom();
        if  (prayerroom == null) {
            throw new Exception("기도실 생성 실패");
        }
}       

Respond 단계

테스트를 통과하는 코드를 작성해서 질문에 대답(성공)

  • 기도실 생성 테스트 케이스를 통과하는 코드를 작성한다.
public class PrayerroomTest {
    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)"); // 예외가 발생하면 실패(X)
            return;
        }
        System.out.println("성공(O)");
    }
}

한 가지 짚고 넘어갈 건!

사실 예시로 작성한 객체가 잘 생성되는지에 대한 테스트는 일반적으로 필요없다.
Java JDK가 망가지지 않는 이상.. 문제될 일이 없기 때문이다.
다만, 생성 로직에서 특별한 처리가 필요할 때(ex. 1000이상의 값이 들어가면 안된다. not nullable이다.)는 있는 게 좋다.
테케는 메소드 사용 설명서라는 측면에서 해당 클래스를 사용하게 될 다른 개발자들에게 도움이 되기 때문이다.

Refine 단계

아이디어를 통합하고 불필요한 것은 제거하고, 모호한 것은 명확히 해서 대답을 정제(리팩토링)

아래와 같은 항목을 리팩토링 적용 대상으로 고민해본다.

  • 소스의 가독성이 적절한가?
  • 중복 코드는 없는가?
  • 메소드명과 변수명이 적절한가?
  • 구조 개선이 필요한 부분은 없는가?

✏️ JUnit4 사용하기

JUnit?

에릭 감마와 켄트 벡이 탄생시킨 JUnit은 현재 전세계적으로 가장 널리 사용되는
Java 단위 테스트 프레임워크다.

구성요소

(1) 단정문 (assertion)

테스트 결과가 예상과 같은지 판별하는 것

@Test // 테스트 메소드로 지정해줌
public void testAssertFalse() {
    assertEquals([message], expected, result);
    assertSame([message], expected, result); // 두 객체가 동일한 객체인지 주소값으로 비교
    assertTrue([message], expected);
    assertNull([message], expected);
}

assertSame 단정문은 주로 동일 객체임을 증명하는 데 쓰인다. 이를테면 캐시 기능을 만들었는데 해당 캐시가 제대로 동작하는지 판단해야한다고 가정해보자. 이때 특정 객체가 캐시에서 가져온 객체와 동일한지 여부를 판단할 수 있다.

cache.add(someObject, KEY);
assertSame("캐시처리 실패!", someObject, cache.lookup(KEY));

싱글톤(특정 클래스의 인스턴스가 오직 하나만 생성될 수 있도록 하는 디자인 패턴)으로 만들어진 객체를 비교할 때 쓰이기도 한다.


(2) 테스트 러너(Test Runner)

작성한 테스트를 실행하기 위해서는 내장된 JUnit 태스트 러너를 이용하는 방법과 테스트 클래스를 직접 실행하는 방법이 있다.

JUnit 내장 Test Runner

java 프로그램에서 실행

org.junit.runner.JUnitCore.runClasses(TestClass1.class, ...);

콘솔에서 실행

$ java org.junit.runner.JUnitCore TestClass1 [...other test classes...]
@RunWith annotation

JUnit에 내장된 러너 대신 해당 클래스에서 테스트를 실행하기 위해 참조하는 클래스를 호출한다.

@RunWith(SpringJUnit4ClassRunner.class)라고 하면
테스트를 진행할 때 JUnit 내장 러너 대신 스프링 부트를 테스트하는 실행자를 실행시킨다고 보면된다. 즉, 스프링 부트 테스트와 JUnit 사이의 연결자 역할을 해준다.

@RunWith(SpringJUnit4ClassRunner.class)
public PrayerroomTest {

    @Test
    public getPrayerroomTest() {
    }
}
(3) test fixture

테스트를 반복적으로 수행할 수 있게 도와주고 매번 동일한 결과를 얻을 수 있게 도와주는
기반이 되는 상태나 환경를 의미한다. 일관된 테스트 실행환경이라고도 함.

For example

  • Preparation of input data and setup/creation of fake or mock objects
  • Loading a database with a specific, known set of data
  • Copying a specific known set of files creating a test fixture will create a set of objects initialized to certain states.
  • @BeforeClass
  • @AfterClass
  • @Before
  • @After

이제 개념 공부는 대강 끝났으니 Kosalaam 프로젝트에 단위테스트부터 추가해보려고 한다. 그 과정은 다음 글에서!

좋은 웹페이지 즐겨찾기