samsung-ga / woody-iOS-tip

🐶 iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포
19 stars 0 forks source link

TDD - TDD Cycle #40

Open samsung-ga opened 2 years ago

samsung-ga commented 2 years ago

Chapter 2 The TDD Cycle

레이먼드 아저씨 책 - iOS Test-Driven Development by Tutorials 의 두번째 챕터 TDD Cycle에서 배운 내용


image

TDD는 아주 간단한 프로세스로 이루어진다.

  1. 실패하는 테스트 코드를 작성한다. (Red)
  2. 테스트가 성공하는 최소단위의 코드를 작성한다. (Green)
  3. 앱과 테스트 코드 두가지 모두 리팩토링 (Refactor)
  4. 모든 피쳐가 완료될떄까지 1~3단계를 반복한다. (Repeat)

Red-Green-Refactor-Repeat (RGRR)

각 단계에서 어떤 작업이 필요한지 하나씩 예제를 통해 보자.

Example

1️⃣ Red : 실패하는 코드 작성하기

CashRegister 클래스를 초기화하는 테스트코드를 작성하자.

스크린샷 2022-09-08 오후 2 19 42

2️⃣ Green : 테스트를 성공하게 만들기

CashRegister클래스를 작성하여 성공하게 만들자.

class CashRegister {
    init() {}
}

3️⃣ Refactor: 코드 정리하기

아래의 테스트 코드가 있다고 해보자.

class CashRegisterTests: XCTestCase {

    func testInit_setsDefaultAvailableFunds() {
        // when
        let sut = CashRegister()

        // then
        XCTAssertEqual(sut.availableFunds, 0)
    }

    func testInitAvailableFunds_setsAvailableFunds() {
        // given
        let availableFunds = Decimal(100)

        // when
        let sut = CashRegister(availableFunds: availableFunds)

        // then
        XCTAssertEqual(sut.availableFunds, availableFunds)
    }

    func testAddItem_oneItem_addsCostToTransactionTotal() {
        // given
        let availableFunds = Decimal(100)
        let sut = CashRegister(availableFunds: availableFunds)

        let itemCost = Decimal(42)

        // when
        sut.addItem(itemCost)

        // then
        XCTAssertEqual(sut.transactionTotal, itemCost)
    }
}

하나씩 자세히 살펴보면 testInitAvailableFunds_setsAvailableFundstestAddItem_oneItem_addsCostToTransactionTotal 두가지 메소드는availableFundsCashRegister를 사용하고 있는 것을 볼 수 있다. 이는 클래스 프로퍼티로 빼줄 수 있다.

class CashRegisterTests: XCTestCase {

    var availableFunds: Decimal!
    var sut: CashRegister!
        // ...
}

위와 같이 프로퍼티로 빼게 될경우, setupteardown 메소드를 이용해서 초기화해줄 수 있다.

setup() : 테스트 메소드가 동작하자마자 호출되는 메소드 (before)

tearDown() : 각 테스트 메소드가 불리우자마자 호출되는 메소드 (after)

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

tearDown메소드에서 nil처리를 하는 이유는 XCTestCase를 서브클래싱하는 클래스는 내부의 테스트 케이스가 모두 끝날때까지 프로퍼티를 가지고 있는다. 프로퍼티의 메모리를 계속 유지하고 있기 때문에 테스트코드를 돌릴 때 메모리와 성능 이슈가 생길 수 있다. 따라서 각 테스트 케이스가 끝나는 시점에 메모리에서 릴리즈해준다.

setUpWithError tearDownWithError 메소드는 setup, tearDown할 때 오류가 날 수 있는 경우에 사용한다.

    func testInitAvailableFunds_setsAvailableFunds() {
        // when
        let sut = CashRegister(availableFunds: availableFunds)

        // then
        XCTAssertEqual(sut.availableFunds, availableFunds)
    }

    func testAddItem_oneItem_addsCostToTransactionTotal() {
        // given
        let itemCost = Decimal(42)

        // when
        sut.addItem(itemCost)

        // then
        XCTAssertEqual(sut.transactionTotal, itemCost)
    }
}

프로퍼티를 밖으로 빼면 테스트 함수가 간단해진다.

4️⃣ Repeat : 다시 반복하자

Basic Concept

1. 테스트 메소드 네이밍 규칙

2. 컴파일 실패 또한 테스트 실패로 간주

3. Refactor할 때 규칙

4. Give/When/Then

func testInitAvailableFunds_setsAvailableFunds() {
  // given
  let availableFunds = Decimal(100)

  // when
  let sut = CashRegister(availableFunds: availableFunds)

  // then
  XCTAssertEqual(sut.availableFunds, availableFunds)
}

위 코드를 해석해보면, 100인 avaiableFunds가 주어졌고 (given) init(availableFunds:)를 통해 초기화한 sut가 생성된 상황이면 (when) sut의 avaiableFunds와 availableFunds는 같다. (then)

sut는 무슨 약자? system under test

정리

완성된 테스트 코드 예제

import XCTest
@testable import TDDPractice

class CashRegisterTests: XCTestCase {

    var availableFunds: Decimal!
    var sut: CashRegister!
    var itemCost: Decimal!

    override func setUp() {
        super.setUp()
        availableFunds = 100
        sut = CashRegister(availableFunds: availableFunds)
        itemCost = Decimal(42)
    }

    override func tearDown() {
        availableFunds = nil
        sut = nil
        itemCost = nil
        super.tearDown()
    }

    func testInit_setsDefaultAvailableFunds() {
        // when
        let sut = CashRegister()

        // then
        XCTAssertEqual(sut.availableFunds, 0)
    }

    func testInitAvailableFunds_setsAvailableFunds() {
        // when
        let sut = CashRegister(availableFunds: availableFunds)

        // then
        XCTAssertEqual(sut.availableFunds, availableFunds)
    }

    func testAddItem_oneItem_addsCostToTransactionTotal() {
        // when
        sut.addItem(itemCost)

        // then
        XCTAssertEqual(sut.transactionTotal, itemCost)
    }

    func testAddItem_twoItems_addsCostsToTransactionTotal() {
        // given
        let itemCost = Decimal(20)
        let expectedTotal = itemCost

        // when
        sut.addItem(itemCost)

        // then
        XCTAssertEqual(sut.transactionTotal, expectedTotal)
    }
}