ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Ch2. The TDD Cycle
    Ray Wenderlich/TDD 2020. 10. 4. 21:55

    4 Steps

    TDD는 4개의 단계가 있다. 이 단계들은 보통 컬러로 표현된다. (color coded)

    • Red: 앱의 코드를 작성하기전에 실패하는 테스트를 작성

    • Green: 테스트를 통과시킬 수 있는 최소한의 코드를 작성

    • Refactor: 앱코드와 테스트코드를 정리 (리팩토링)

    • Repeat: 모든 피쳐들을 구현할 때까지 이 사이클을 반복



    Red: Write a failing test

    Production코드를 작성하기 전에, 실패하는 테스트 코드를 먼저 작성하자.

    class CashRegisterTests: XCTestCase {
    		func testInit_createsCashRegister() {
    		  XCTAssertNotNil(CashRegister())
    		}
    }
    
    CashRegisterTests.defaultTestSuite.run()
    

    testInit_createsCashRegister

    test: 테스트할 메소드의 이름앞에 붙인다. 여기선 init메소드를 테스트한다. 그리

    test다음에 테스트할 메소드명을 붙인다. 그 뒤로 특별한 조건이 필요하면 underscore(_)로 구분한다. 위 경우는 조건이 없어서 안썼다. 마지막에는 테스트로 기대하는 결과를 적는다. 예문에선 cashRegister가 생성되기를 기대하고 있다.

    이대로는 컴파일이 되지 않을 것이다. CashRegister클래스가 없으니깐! 하지만 컴파일이 되지 않는 것도 Test Fail로 간주한다. 즉, red step을 완료했다.



    Green: Make the test pass

    테스트를 통과시킬 최소한의 코드를 작성해보자.

    class CashRegister {
      
    } 

    테스트가 통과할 것이다. 다음 단계는 리팩터링을 하는 것이다.


    Refactor: Clean up your code

    리팩터링 단계에서 테스트코드와 프로덕트코드를 모두 개선한다. 신경써야할 몇가지를 살펴보자.

    • Duplicate logic: 중복된 로직을 제거하기 위해 다른 프로퍼티, 메소드, 클래스를 가져올 수 있는가?
    • Comments: 주석은 코드가 왜 필요한지를 설명해주어야 한다. 무엇이 작동하는지를 설명하는 주석은 제거해야 한다. 어떻게?? 덩치가 큰 메소드를 네이밍을 잘한 여러개의 메소드로 쪼개주어야 한다. 프로퍼티와 메소드의 네이밍을 잘 해주도록 하자.
    • Code smells: 간혹 특정 코드 덩어리가 잘못되어 보일 수 있다. 직감을 따라 이 "code smells"를 제거해보자. 예를 들어, 로직이 너무 많은 가정을 하고 있거나, 하드코딩된 스트링 등이 있다. 위(Comments)에서 사용한 방법을 여기에도 적용하자. 메소드, 클래스, 프로퍼티들을 다시 네이밍하고 코드의 구조를 다시 짜자.

    CashRegister와 CashRegisterTests는 더 이상 보강할 로직이 없다. 따라서 이번단계도 끝났다.

    TDD의 다음 단계는 Repeat이다


    Repeat: Do it again

    1사이클을 마쳤다. 우리의 다음 목표는

    Write an initializer that accepts availableFunds.


    TDDing init(availableFunds:)

    사이클의 첫 번째인 실패하는 테스트를 작성하자. 컴파일에러가 발생하는 것도 테스트 실패다.

    func testInitAvailableFunds_setsAvailableFunds() {
      // given
      let availableFunds = Decimal(100)
      
      // when
      let sut = CashRegister(availableFunds: availableFunds)
      
      // then
      XCTAssertEqual(sut.availableFunds, availableFunds)
    }
    

    첫 번째 사이클의 테스트코드보다 복잡하다. 그래서 세 가지로 쪼개보았다: given, when, then. 유닛테스트를 이런 관점에서 생각해보면 유용하다.

    • Given a certain condition
    • When a certain action happens
    • Then an expected result occurs

    이 경우 availableFunds라는 Decimal(100)의 값을 given했다. init(availableFuncds:)를 사용하여 sut을 생성했을 때, When, sut.availableFunds와 availableFunds가 동일할 것을 예상하고 있다.Then

    sut은 system under test의 약어로, TDD에서 자주 쓰이는 표현이다. 무엇을 테스트하던 자주 쓴다.

    red step을 마쳤다.

    다음은 아래 코드를 CashRegister에 추가하자.

    var availableFunds: Decimal
    
    init(availableFunds: Decimal = 0) {
      self.availableFunds = availableFunds
    }
    

    테스트가 성공할 것이다.

    다음 차례로, 테스트 코드와 앱 코드를 개선할 차례다. 먼저, 테스트 코드를 살펴보자.

    testInit_createsCashRegister는 더이상 의미가 없다. init()메소드가 더 이상 없기 때문이다. 이 테스트는 그저 default value와 함께 init(availableFunds:)를 호출하는 것에 불과하다.

    testInit_createsCashRegister를 통째로 덜어내자.

    앱 코드를 살펴보자. availableFunds에 0이라는 디폴트 값을 주는게 맞다고 생각하나? testInit과 testInitAvailableFunds를 둘 다 통과시키기엔 좋았지만, 이 클래스에 이게 정말 필요한가?

    결국, 이 것은 Design decision이다.

    • 디폴트 파라미터를 계속 사용하기로 결정했다면, testInit_setsDefaultAvailableFunds 테스트를 추가하는 걸 생각해봐야 한다.
    • 디폴트 파라미터를 갖는게 이상하다고 생각한다면, 이를 제거해주면 된다.

    이 경우에는 디폴트 파라미터가 적절하다고 보이지 않는다. 그래서 코드를 수정했다.

    init(availableFunds: Decimal) {
    

    테스트는 여전히 모두 통과한다.

    testInitAvailableFunds이 통과했다는 사실은 init(availableFunds:)메소드 리팩터링이 프로그램의 현재 기능을 무너뜨리지 않았음을 의미한다. 리팩터링에 이런 확신을 주는 것이 TDD의 가장 큰 장점이다.

    테스트를 하다보면 중복되는 로직이 발생한다. 예를 들어, 테스트를 할 때마다 아래 코드를 사용할 수 있다.

    let availableFunds = Decimal(100)
    let sut = CashRegister(availableFunds: availableFunds)
    

    이 두개의 프로퍼티를 멤버변수로 가질 수 있다. 테스트 코드에도 프로덕트 코드처럼 얼마든지 필요한 프로퍼티와 메소드를 추가할 수 있다. 사실 테스트를 도와줄 특별한 메소드가 2개 있다: setUp(), tearDown()

    setUp()은 테스트 메소드가 호출되기 직전에 호출되고, tearDown()은 테스트 메소드가 끝난 직후 호출된다.

    이 두 개의 메소드는 중복되는 로직은 처리할 가장 완벽한 장소라고 볼 수 있다.

    var availableFunds: Decimal!
    var sut: CashRegister!
    
    override func setUp() {
      super.setUp()
      availableFunds = 100
      sut = CashRegister(availableFunds: availableFunds)
    }
    
    override func tearDown() {
      availableFunds = nil
      sut = nil
      super.tearDown()
    } 

    setUp()에서 생성한 값들은 항상 tearDown()에서 nil로 설정해주자. 이는 XCTest프레임워크가 작동하는 방식과 관련이 있다. XCTestCase의 서브클래스를 테스트 타겟에서 생성한 뒤, 모든 테스트 케이스의 수행이 끝날 때까지 릴리즈되지 않는다. 따라서, 많은 케이스를 수행하는 경우, 프로퍼티들을 메모리에서 해제해주지 않으면 메모리를 필요 이상으로 잡아먹게 된다. 이는 메모리와 성능 이슈로 이어질 수 있다.




    'Ray Wenderlich > TDD' 카테고리의 다른 글

    Ch1. TDD란 무엇인가?  (0) 2020.10.04
Designed by Tistory.