TDD(Test Driven Dvelopment)

Posted by yunki kim on November 7, 2021

TDD는 테스트케이스를 작성한 후 실제 코드를 개발해 리펙토링을 한다. 

기존 방식:

TDD:

  애자일에서는 불확실성이 높을 때 피드백과 협력이 중요하다고 한다. TDD역시 이런 이유로 반복적인 피드백과, 협력을 중요시 한다. 

  만약 특정 기능을 하는 코드를 반복적으로 작성한 경험이 있거나 결과가 명백하다면 TDD를 하지 않아도 된다. 하지만 다음과 같은 경우에서는 TDD를 하는 것이 적절하다.

    1. 처음해보는 프로그램 주제 - 나에 대한 불확실성이 높을 경우

    2. 고객 요구사항이 지속적으로 바뀔경우 - 외부적인 불확실성이 높을 경우

    3. 개발 중에 코드를 많이 바꾸어야 하는 경우

    4.  누가 유지보수를 할지 모르는 경우

 

Test의 명사화

  TDD에서 test는 동사가 아닌 명사이다. 즉, 동사처럼 그 순간에만 어떤 대상을 위해 존재하는 것이 아닌 명사처럼 이후에도 소유할 수 있다. 따라서 test는 하고 패스하는 것이 아닌 남들이 봐야하는, 남들이 실행시켜봐야 하는 것이다.

  이것이 TDD가 협력을 증진시키는 이유이다. 다른 사람의 코드를 쉽게 접근할 수 있고 이해가 빨라진다.

 

Red, Green, Refactor

  Red, Green, Refactor는 TDD를 수행하는 하나의 프래임워크이다. 개발자는 이 프레임워크를 사용해 테스트 케이스를 만들고, 코드를 작성하고, 코드를 최적화 할 수 있다.

  Red - 개발을 해야할 대상에 대해 생각한다.

  Green - 어떻게 테스트케이스를 통과할지를 고민한다.

  Refactor - 현재 존재하는 코드를 어떻게 최적화할지를 고민한다.

  Red

    Red는 Red, Green, Refactor사이클 중에서 시작점이다. 이 단계에서는 기능 구현을 알리는 테스트를 작성하게 된다. 이 테스트는 테스트의 예상 결과가 나왔을 때 패스된다.

  Green

    Green은 코드를 실제로 작성해 Red에서 작성했던 테스트 케이스를 통과하도록 하는 과정이다. 이 단계에서는 코드 최적화와는 관련없이 그저 테스트 케이스가 통과될 정도의 코드만 작성하면 된다.

  Refactor

    아직 Green에 있는 단계이다. 이 단계에서는 코드를 어떻게 더 효율적으로 작성할지를 고민하면 된다. 이 단계에서 test suite(test case의 집합)를 리팩토링 한다면 좋은 테스트의 성질인 MC-FIRE에 대해 고민해야 한다. 코드를 리팩토링 하고 싶다면 어떻게 하면 동일한 output을 내보내되 더 명시적이고 빠른 코드를 작성할 수 있을지를 고민하면 된다.

  Refactor단계는 필수가 아니다. 하지만 Green단계에서 다시 Red단계로 넘어가기 전에 다음과 같은 질문을 하고 리팩토링을 고민하는 것이 좋다.

    1.  현재의 test suite를 더 명시적으로 만들 수 있나?

    2. 현재의 test suite가 믿을만한 피드백을 제공하고 있나?

    3. 현재의 코드와 test suite에서 중복된 케이스가 존재한가?

    4. 현재의 코드를 더 명시적으로 바꿀 수 있나?

    5. 좀 더 효율적인 방법이 있나?

    6. 나의 테스트가 독립적인가?

 

좋은 테스트의 조건

  TDD에서는 수 많은 유닛 테스트가 요구된다. 따라서 어떤 테스트가 좋은 테스트인지 알 필요가 있다.

  Test suite를 사용하는 이유는 테스트들을 이용해 배포 전에 에러를 캐치하기 위함이다. 

  좋은 테스트는 실행 시간이 길지 않으며 테스트가 통과되면 소프트웨어가 예상된 동작을 한다는 신뢰성을 주어야 한다. 만약 테스트가 버그를 발견한다면 이 테스트는 개발자가 버그의 원인을 찾을 수 있는 피드백을 주어야 한다.

  좋은 테스트의 좋건은 다음과 같다.

    1. Fast

    2. Complete

    3. Reliable

    4. Isolated

    5. Maintainable

    6. Expressive

  Fast

    Full-stack 웹 애플리케이션에 대한 테스트는 크게 유닛 테스트, 통합 테스트(integration tests)로 나뉘어져 있다. 유닛 테스트는 빠르고, 통합 테스트는 유닛 테스트에 비해 느리다. 만약 하나의 test suite가 다량의 통합 테스트를 포함하고 있다면 수 분, 심하면 몇시간을 기다려야 할 수도 있다.

    빠른 test suite는 피드백을 더 빨리 주지 때문에 개발 프로세스가 느린 test suite에 비해 더 효율적이다. 만약 임의의 test suite A가 5분이 걸리고 test suite B가 30초가 걸린다고 해보자. A, B가 같은 코드에 대한 테스트라 했때 A, B를 5번 실행시키면 B는 A에 비해 약 22분을 절약할 수 있다.

  Complete

      하나의 애플리케이션에 해당하는 코드를 test suite가 전부 커버할 수 있다면 코드의 변경, 추가에 대한 어떠한 에러도 잡아낼 수 있다. 이런 온전한 test suite는 소프트웨어가 예상한 대로 동작한다는 신뢰성을 준다. 

  Reliable

      신뢰성 있는 테스트는 해당 테스트 스포트 밖의 변화와는 관련없이 지속적인 피드백을 주어야 한다. 신뢰성이 없는 테스트는 간헐적으로 실패를 하고 개발자가 애플리케이션에 가한 수정에 대한 피드백을 주지 않는다. 만약 테스트가 신뢰성이 없다면 온전히 같은 상황에서 버그 수정여부 확인을 위해 여러번의 테스트를 돌렸을 때 어떤것을 실패하고 어떤것은 성공할 것이다.

   Isolated

      하나의 test suite는 다른 테스트들과 충돌이 나지 않아야 한다. 따라서 일부 상황에서는 테스트를 진행한 후 기존 데이터를 정리해야 할 수도 있다.

  Maintainable

      test suite는 조작이 간단해야 한다. 쉽게 추가하고, 바꾸고, 지울수 있어야 한다. 만약 새로운 기능에 대해 테스트를 추가할 수 없다면 해당 test suite는 비효율 적이고, 온전하지 않게 된다.

      test suite를 유지, 관리할 수 있는 방법은 코딩 모범 사례를 따르며 사용자와 팀에 적합한 일관된 프로세스로 개발을 하는 것이다.

    Expressive

      읽기 쉬운 test suite는 좋은 문서이다. 코드를 작성할때는 항상 테스의 대상이 되는 기능을 잘 서술해야 한다. test suite를 만들때는 다른 개발자들이 충분히 알아볼 수 있을 정도로 서술해아 하며 해당 웹 애플리케이션의 목적을 이해되게 해야 한다. 또 한 test suite는 소프트웨어의 한 부분이기 때문에 README나 문서들 보다도 더 빠르게 최신 버전을 유지해야 한다. 

    

TDD 개발 방식의 장점

  1. 낮은 의존성

     TDD는 코드의 재사용 보장을 명시하고 TDD를 통한 SW 개발은 철저한 모듈화가 이뤄진다. 이는 종속성과 의존성이 낮은 모듈로 조합된 SW 개발을 가능하게 하고 필요에 따라 모듈을 추가하고 제거해도 SW전체 구조에 영향을 미치지 않는다.

  2. 재설계 시간 단축

      테스트코드 작성을 통해 현재 어떤 것이 필요한지를 명확히 할 수 있다. 따라서 중간에 설계를 변경하는 일이 줄어든다.

  3. 디버깅 시간의 단축.

      유닛테스트를 통해 버그가 발생했을 때 어느 부분에서 문제가 발생한건지를 명확히 할수 있다.

   4. 테스트 문서의 대체 가능

       SI에서는 어떤 요소들이 테스트 되었는지 테스트 정의서를 만든다. 이는 단순한 통함 테스트 문서이다. 하지만 TDD의 경우 테스팅을 자동화 하고 보다 정확한 테스트 근거를 산출할 수 있다.

    5. 추가 구현의 용이

        기존 SW에 어떤 기능을 추가할때 추가한 코드가 기존 코드에 어떤 영향을 미칠지 모른다는 문제가 발생한다. 하지만 TDD를 사용하면 유닛 테스팅을 통해 테스트 기간을 단축시킬 수 있다. 

 

TDD 개발 방식의 단점

    처음 부터 2개의 코드를 짜야하기 때문에 일반적인 개발 방식에 비해 10~30%정도 개발 시간이 늘어난다.

 

AAA pattern(Arrage, Act, Assert)

  테스트 코드를 작성할 때 AAA 패턴을 사용하면 가독성이 향상되고 다른 사람의 코드를 이해하는데 도움이 된다.

  AAA패턴은 테스트 코드를 3단계로 나누어 작성한다.

    Arrange(준비): 테스트를 실행하기 전에 필요한 것들을 준비한다. 객체 생성, 테스트 전에 호출할 API 호출 등.

    Act(실행): 테스트 코드를 실행한다.

    Assert(단언): 실행한 코드가 예상대로 작동했는지를 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//하나의 유닛 테스트는 test()메서드에서 이루어 진다.
//test()의 첫번쨰 인자는 스트링이며 어떤 테스트인지를 서술한다.
//두번째 인자는 콜백이며 여기서 실제 테스트가 실행된다.
//세번째 인자는 밀리세컨드 단위의 시간을 넣을 수 있고
//이는 테스트를 진행할 시간을 의미한다.
//만약 1000을 명시했는데 해당 테스트가 1초안에 끝나지 않았다면
//테스트는 실패한다.
test("convert array of country data objects to array of countries", ()=>{
    //AAA패턴 사용
    //arrange
    const inputObject = [
      {name"Argentina", capital: "Buenos Aires"},
      {name"Belize", capital: "Belmopan"},
      {name"Bolivia", capital: "Sucre"}
      ]
    const expectedValue = ["Argentina","Belize","Bolivia"]
    
    //act
    const actualValue = countryExtractor(inputObject)
    
    //assertions
    expect(actualValue).toEqual(expectedValue);
    //toEqual()은 깊은 비교를 하고 tobe()는 얕은 비교를 한다.
});
cs

jest를 사용해 assertion 단계에서 결과 검증하기

1
2
3
4
5
6
expect(actualValue).toEqual(expectedValue)
expect(actualValue[0]).toBe("Argentina")
expect(actualValue).toContain("Belize");//actualValue가 "Belize"를 포함하고 있나
expect(actualValue[2=== "Bolivia").toBeTruthy();//expect()의 조건이 참인가
//not은 !과 같다. toBeDefined()는 undefined여부 검증
expect(actualValue[3]).not.toBeDefined();//actualValue의 길이가 3인가. 만약 3이면 [3]은 undefined
cs