관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Clean Code(클린 코드) - 8. Unit Test (단위 테스트) 본문

Clean Code (클린 코드)

[iOS - swift] Clean Code(클린 코드) - 8. Unit Test (단위 테스트)

jake-kim 2021. 11. 19. 22:39

Unit Test 코드가 중요한 이유

  • 테스트 케이스가 있으면, 실제 코드를 변경하는 것이 두렵지 않은 장점이 존재
  • 유연성, 유지보수성, 재사용성을 제공
    • 테스트 케이스가 있으면, 실제 코드를 변경할 때 테스트 케이스를 사용하여 수정한 코드가 잘 돌아가는지 테스트할 수 있기 때문에 결함율이 낮아지는 효과 (= 유연성과 유지보수성)
    • 테스트 케이스를 작성해 놓으면 해당 테스트 케이스는 계속 사용할 수 있으므로 재사용성 제공
  • 아키텍처가 아무리 유연하고 설계를 아무리 잘 나누었더라도, 테스트 케이스가 없다면 변경에 주저할 수 밖에 없는 상황이 발생
  • 쌓인 테스트 케이스, 즉 테스트 슈트는 설계와 아키텍처를 변경하기 쉬워지므로, 이런 것들을 최대한 깨끗하게 보존하는 열쇠

TDD의 개념

Test Driven Development

  • TDD의 3가지 단계

    1) 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는 것

    2) 컴파일은 실패하지 않으면서 실패하는 정도로만 단위 테스트 작성

    3) 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성

  • 위 3가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 형성
  • 실제 코드를 전부 테스트하는 테스트 케이스가 생성
    • 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제도 유발 > 테스트 케이스도 깨끗함을 유지하는 것이 중요

깨끗한 테스트 코드를 유지해야 하는 이유

  • 지저분한 테스트 코드를 작성하느니 테스트 코드가 없는 편이 나을 정도의 영향
    • 실제 코드가 변경되면 테스트 코드도 변경되어야 하는데, 테스트 코드가 복잡할수록 테스트 코드를 변경하기가 어려운 상황
    • 실제 코드보다 테스트 코드를 변경하는 시간이 더욱 오래걸리는 현상
  • 만약 쌓여있는 테스트 코드, 즉 `테스트 슈트`가 없다면 자신이 수정한 코드가 제대로 도는지 확인이 어려움으로 결함율이 높아지는 현상이 발생함으로 깔끔한 테스트 코드를 유지하는것이 중요
  • 급한 상황이라도 테스트 코드를 실제 코드와 같이 깨끗한 코드로 유지하는 태도가 중요

깨끗한 테스트 코드를 유지하는 방법

  • 가장 중요한점은 가독성
    • 가독성은 실제 코드보다 테스트 코드에 더욱 중요
    • 테스트 코드는 최소의 표현으로 많은 것을 나타내야함

ex) 중복되는 코드가 많은 경우

WRONG

- crawler에게 페이지를 주고, 해당 페이지에 값을 입력 후 예상하는 값을 얻어오는지 확인하는 테스트 코드

class ExTestCodeTests: XCTestCase {
    
    var crawler: Crawler!
    var request: Request!

    override func setUpWithError() throws {
        crawler = Crawler()
        request = Request()
    }
    
    func test_whenNormally_thenGetPageHieratchyAsXml() {
        crawler.addPage("root", "PageOne")
        crawler.addPage("root", "PageTwo")
        crawler.addPage("root", "PageThree")
        
        request.setResource("root")
        request.addInput("type", "page")
        
        let responder = Responder()
        let response = responder.makeResponse(request)
        let xml = response.getContent()
        
        XCTAssertEqual("test/xml", response.getContentType())
        XCTAssertTrue("<name>PageOne</name>".contains(xml))
        XCTAssertTrue("<name>PageTwo</name>".contains(xml))
        XCTAssertTrue("<name>PageThree</name>".contains(xml))
    }
}

RIGHT

ex) 불필요하게 중복되는 코드를 없앤 경우

- 위 코드와 비교하면 훨씬 가독성이 있는 코드로 변환

- 나중에 테스트 코드를 수정할 때도 더욱 변경하기 쉬운 코드

    func test_whenNormally_thenGetPageHieratchyAsXml() {
        makePage("pageOne", "pageOne.ChildOne", "pageTwo")
        
        let response = submitRequest("root", "type:pages")
        
        XCTAssert(response.isXml)
        XCTAssertTrue(response.xml.contains("<name>PageOne</name>") && response.xml.contains("<name>PageTwo</name>") && response.xml.contains("<name>ChildOne</name>"))
    }

ex2) 온도가 급겹하게 떨어지면, 경보, 온풍기, 송풍기가 모두 가동되는지 확인하는 코드

WRONG

- 반복되는 코드가 존재

- heaterState()라는 상태를 보고서는 왼쪽으로 눈길을 돌려 XCTAssertTrue를 읽어야 하는 번거로움 존재

func test_whenTurhnOnLowTempAlarmAtThreashold_thenOperatingDevice() {
    hw.setTemp(WAY_TOO_COLD)
    controller.tic()
    XCTAssertTrue(hw.heaterState())
    XCTAssertTrue(hw.blowerState())
    XCTAssertFalse(hw.coolerState())
    XCTAssertFalse(hw.highTempAlarm())
    XCTAssertTrue(hw.lowTempAlaram())
}

RIGHT

- 반복되는 코드 리펙토링

- wayTooCold()라는 함수를 만들어 숨겨서 사용

- H 대문자는 켜짐, h 소문자는 꺼짐을 의미

func getStage() {
	var state = ""
    state += heater ? "H" : "h"
    state += blower ? "B" : "b"
    state += cooler ? "C" : "c"
    state += hiTempAlarm ? "H" : "h"
    state = loTempAlarm ? "L" : "l"
    return state
}

func test_whenTurnOnCoolerAndBlowerIfTooHot_thenOperatingDevice() {
	tooHot()
    XCTAssertEqual("hBChl", hw.getState())
}

func test_whenTurnOnHeaterAndBlowerIfTooCold_thenOperatingDevice() {
	tooCold()
    XCTAssertEqual("HBchl", hw.getState())
}

func test_whenTurnOnHiTempAlarmAtThreshold_thenOperatingDevice() {
	wayTooHot()
    XCTAssertEqual("hBCHl", hw.getState())
}

func test_whenTurnOnLoTempAlarmAtThreshold_thenOperatingDevice() {
	wayTooCold()
    XCTAssertEqual("HBcHL", hw.getState())
}

테스트 함수 하나 당 assert 하나

  • 위 예제 코드와 같이 테스트 함수 하나 당 assert 구문이 하나이면 코드를 이해하고 쉽고 빠른 장점이 존재
  • 함수 이름을 given, when, then으로 나누어서 사용하여 가독성을 높일 것

WRONG

- 함수 내부 블록에 given - when - then으로 되어있지 않은 경우

func test_whenNormally_thenGetPageHieratchyAsXml2() {
    makePage("pageOne", "pageOne.ChildOne", "pageTwo")
    
    let response = submitRequest("root", "type:pages")
    
    XCTAssert(response.isXml)
    XCTAssertTrue(response.xml.contains("<name>PageOne</name>") && response.xml.contains("<name>PageTwo</name>") && response.xml.contains("<name>ChildOne</name>"))
}

RIGHT

- 함수 내부 안에 given - when - then으로 되어 있어서 이해하기 훨씬 쉬운 코드

func test_whenNormally_thenGetPageHierarchyAsXml() {
    givenPages("pageOne", "pageOne.ChildOne", "pageTwo")
    
    whenRequestIsIssued("root", "type: pages")
    
    thenResponseSouldBeXML()
}

테스트 함수 하나 당 개념 하나

  • 테스트 함수마다 한 개념만 테스트하는 의미

ex) 이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수

WRONG

- 테스트 함수 하나에 여러개의 개념을 테스트하는 형태

- 아래와 같은 테스트 함수는 3개로 분리할 것

- assert 문 수가 많은 상태

func testAddMonths() {
    let d1 = SerialDate.createInstance(31, 5, 2004)
    
    let d2 = SerialDate.addMonths(1, d1)
    XCTAssertEqual(30, d2.getDayOfMonth())
    XCTAssertEqual(6, d2.getMonth())
    XCTAssertEqual(2004, d2.getYYYY())
    
    let d3 = SerialDate.addMonths(2, d1)
    XCTAssertEqual(30, d3.getDayOfMonth())
    XCTAssertEqual(6, d3.getMonth())
    XCTAssertEqual(2004, d3.getYYYY())
    
    let d4 = SerialDate.addMonths(2, SerialDate.addMonths(1, d1))
    XCTAssertEqual(30, d4.getDayOfMonth())
    XCTAssertEqual(6, d4.getMonth())
    XCTAssertEqual(2004, d4.getYYYY())
}

Unit Test의 FIRST 규칙

  • First
    • 자주 실행시키는 테스트 함수가 오래걸린다면 생산성, 코드 정리를 꺼리게 되므로 빠르게 실행되는 코드로 설계
  • Independent
    • 각 테스트가 서로 종속적이면, 하나가 실패할 경우 다른것에도 영향을 미치므로 서로 의존하지 않도록 설정
  • Repeatable
    • 테스트는 어느 환경에서도 반복 가능하도록 설계 (네트워크 연결이 되지 않은 상황에서도 실행 되도록 설계)
  • Self-Validating
    • 테스트는 Bool값으로 결과를 내야하므로, log값을 읽고 해석하게 되면 주관적이므로 설계하지 말것
  • Timely
    • 테스트는 실제 코드를 구현하기 직전에 구현해야 적합
    • 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어려운 경우가 많이 존재

* 참고: Clean Code (로버트 C. 마틴)

Comments