본문 바로가기
개발/swift

TDD in iOS (#1 - UnitTest)

by 꼬마상어 2018. 5. 31.
반응형

TDD란?

Test-Driven Development의 약자로 말그대로 테스트코드를 먼저 작성 후 실제 개발을 하는 것을 말합니다.

테스트 주도 개발의 배경이 되는 생각은 설계할 동안 작성 중인 코드가 무엇을 해야 할지를 생각하게 하는 것입니다.

테스트 주도 개발의 작업 접근법은 테스트를 하나 만들고 실행해서 실패하는지 확인한 후 이 테스트를 통과할 코드를 작성하는 것입니다. 통과하면 다음 테스트를 작성하는 것을 반복합니다.


 

위의 그림을 보면 TDD의 프로세스를 이해하기 쉽습니다.

  • Red (적색 단계)

    • 실패하는 테스트 만들기
    • 결과적으로 원하는 방법으로 코드를 작성하여 실패하는 테스트
    • 이 테스트 코드를 바탕으로 실제 개발 코드를 작성합니다.
  • Green (녹색 단계)

    • 적색 단계에서 작성한 테스트를 바탕으로 실제 개발 코드를 작성하여 테스트 코드를 성공하는 개발 코드를 작성합니다.
    • 이 단계에서 중복코드, 예쁜 네이밍을 작성하는 것을 두려워하지않습니다. 일단 막 씁니다.
  • Refactor (리펙토링)

    • 녹색 단계에서 작성한 코드에서 반복되는 코드, 긴 메소드명, 긴 매개변수 목록 등을 보기좋게 리펙토링합니다.

 

YAGNI

= 지금 당장 필요 없으면 앞으로를 위해 준비할 필요가 없다.

= Ya Ain't Gonna Need It

TDD에서 아무리 강조해도 지나치치 않는 단어입니다.

실제 코드를 작성하다보면 "아 이거 나중에 쓸것같은데..", "혹시 모르니까..?" 라는 생각에 추가로 인터페이스를 작성해 놓는다던가, 주석처리를 하는 경우가 있습니다.

TDD에서 위에 언급한 TDD의 프로세스를 수행하기 위해서는 적색단계 전에 우선으로 해야 할 것이 요구사항 정리 입니다.

필요한 것을 알게 되면 이를 충족시키려고 어떤 코드를 사용해야 할지 결정할 수 있습니다.

어떠한 기능을 제공할 것인지 개발자의 머리속에 정리해 놓지 않으면 혹시나 하는 마음에 추가로 코드를 작성할 것입니다.

나중에 실제로 그 코드를 사용할지 안사용할지 모르는데 말이죠.

 

TDD의 장점
  • 유지보수가 쉽다.

  • 요구사항이 정의되어있어 의도대로 코딩이 진행된다.

  • 필요없는 코드는 없다.

  • 개발 퇴행성 버그 방지!

  • 한번 작성해 놓으면 나중에 계속 테스트 할 수 있다. 물론 AOTOMATIC으로!!!!!!!

     

TDD In iOS

iOS용 앱을 개발할 때 사용하는 Tool인 XCode에서 제공하는 Test의 종류는 두가지입니다.

  • UnitTest

    • logical한 부분을 테스트 합니다.
    • ex) 피보나치알고리즘, 모델
  • UITest

    • UI에 관련된 부분을 테스트 합니다.
    • ex) 어떠한 버튼을 눌렀을 경우 텍스트가 올바르게 변경되는가

 

UnitTest (단위테스트)

말보다는 행동인 것 같습니다..

 

  1. Test Target 생성

기존 프로젝트가 있는 분은 [File > New > Target > iOS > UnitTest]를 생성합니다.


 

새로운 프로젝트를 생성하려면 아래 Include Unit Tests를 체크후 프로젝트를 생성하세요.

그럼 프로젝트이름Tests 폴더 아래에 테스트 파일이 생성되었습니다.

 

기본 UnitTest 템플릿이 작성되어 있습니다.

import XCTest

class testTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    
    func testExample() {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }
    
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
}

  • import XCTest

    • 단위테스트를 하기 위한 프레임워크입니다.
  • setUp()

    • 테스트를 하기 전에 초기화를 하는 부분입니다.
    • 혹시 테스트를 위해 초기화 되어야 할 값이 있다면 이 메소드 안에 코드를 작성합니다.
  • tearDown()

    • 테스트가 시작되고 난 후에 어떠한 값이든 release합니다.
    • super.tearDown() 전에 코드를 작성합니다.
  • testExample(), testPerformanceExample()

    • 테스트를 위한 예제 탬플릿입니다.
    • 삭제하여 다른 function을 작성하여도 무방합니다.
  • self.measure {}

    • 만약 성능을 테스트 해보고 싶다면 self.measure안에 클로저로 코드를 작성합니다.
    • 시간, 쓰레드 등을 측정 가능합니다.

 

  1. 테스트 코드 작성해보기

실질적인 적색단계에 진입하기 전에 요구사항을 정리해볼까 합니다.

저는 Integer형을 입력하면 금액표시처럼 나타나게 하는 기능을 넣고 싶습니다.

(물론 formatter로 사용가능하지만, 그 이외의 방법으로 구현해 보고자 합니다.)

ex) 1000 입력시 1,000로 출력

 

적색단계를 시작해봅시다.

테스트 작성시에 prefix로 test를 작성해 주어야 합니다.

그렇지 않으면 테스트가 아니라고 간주하여 스킵합니다.

테스트를 작성할 때는 아래와 같은 순서로 작성하는 것이 도움이 됩니다.

  1. given

    • 당신이 필요한 어떠한 값을 셋업합니다.
  2. when

    • 테스트 하고자 하는 코드를 불러옵니다.
  3. then

    • 당신이 기대하는 결과값과 비교하여 결과를 출력하게 합니다.

 

func testChangeMoneyFromInt() {
    // 1. given
    let vc = ViewController()

    // 2. when
    let moneyValue = vc.transferToMoney(value: 1000)
    let expectValue = "1,000"

    // 3. then
    XCTAssertEqual(moneyValue, expectValue, "값이 일치하지 않습니다.")
}

 

XCTest 프레임워크에는 테스트를 위한 메소드가 존재합니다.

결과값을 비교할 수 있도록 인터페이스가 존재하며 실패하면 메세지를 뿜어줍니다.

참고 : https://developer.apple.com/documentation/xctest/xctassertequal

해당 테스트를 작성 후 cmd + U를 눌러 테스트를 run합니다.

혹은 해당 func 옆의 다이아몬드 아이콘을 눌러 테스트를 run합니다.

 

해당 function에 대해 아무런 개발 코드를 작성하지 않았으니 이는 Fail합니다.

녹색단계

실패하는 테스트 코드를 작성했으니 이를 기반으로 실제 개발코드를 작성해 봅시다.

이 단계에서는 효율성을 고려하지 않습니다.

class ViewController: UIViewController {
	
    ...
    
	func transferToMoney(value: Int) -> String {
		let valueString = String(value)
		let howmanyLoop = valueString.count / 3
		
		var substrings = [String]()
		var loop = 0
		
		while loop <= howmanyLoop, howmanyLoop != 0 {
			let startIndexs: String.Index
			if loop == howmanyLoop {
				// 마지막 루프라면
				startIndexs = valueString.startIndex
			} else {
				startIndexs = valueString.index(valueString.endIndex, offsetBy: -3 * (loop+1))
			}
			
			let endIndexs = valueString.index(valueString.endIndex, offsetBy: -3 * loop)
			
			if endIndexs != startIndexs {
				let values = String(valueString[startIndexs..<endIndexs])
				
				substrings.insert(values, at: 0)
				
			}
			loop += 1
		}
		return substrings.joined(separator: ",")
	}
}

테스트를 run하면 success결과를 내어주는 개발 코드를 완성하였습니다.

끝에서부터 3자리씩 잘라 array에 넣고 나중에 join으로 붙이는 코드 입니다.

맘에 들지않는 코드가 완성되었군요 ㅎㅎ..

 

리펙토링 : 예쁜 코드 만들기^0^

불필요한 변수, 네이밍, 효율성을 생각하여 리펙토링합니다.

똑같은 기능이지만 로직상의 개선을 진행합니다.

func transferToMoney(value: Int) -> String {
    let valueString = String(value)
    let howManyLoop = valueString.count / 3

    guard howManyLoop > 0 else {
        return valueString
    }

    var substrings = [String]()

    (0...howManyLoop).forEach({ loop in
        let startIndexs = loop == howManyLoop ? valueString.startIndex : valueString.index(valueString.endIndex, offsetBy: -3 * (loop+1))

        let endIndexs = valueString.index(valueString.endIndex, offsetBy: -3 * loop)

        if endIndexs != startIndexs {
            substrings.insert(String(valueString[startIndexs..<endIndexs]),
                              at: 0)
        }
    })

    return substrings.joined(separator: ",")
}

테스트 결과는 똑같이 success이지만, 코드상의 개선이 이루어졌습니다!

이렇게 만들고, 나중에 또다시 리펙토링이 이루어질때도 코드를 작성하고 다이아몬드 버튼만 누르면 빠르게 테스트가 가능합니다.

 

TDD를 R&D하며 느낀 소감

TDD를 R&D하기전까지 굉장한 두려움을 가지고 있었습니다.

수박 겉핥기식으로만 TDD를 접해왔기 때문에 "실무에는 적용이 어렵다." "하면 좋지만, 현실적으로 불가능하다." 이런 말들을 많이 들어왔고, 저또한 그렇게 생각했습니다.

R&D를 하고 난 뒤에 제 생각은 많이 변했습니다.

물론 테스트 코드를 작성하는데에 있어서 꽤 시간을 들여야 하겠지요.

하지만 나중에 안정성과 유지보수를 생각하면 멋진 방법론이라는 생각이 들었습니다.

한번 작성한 테스트 케이스에 대해서는 나중에 똑같은 실수가 나오지 않을 것이고, QA하는데에 시간도 적게 걸리고 부담도 적어질 듯 합니다.

(아직은 저도 실무에 적용하지않아서 UI로직과 로직컬한 것을 어떻게 묶어서 처리할지를 고민해야되겠지만.. )

멋있어 ! TDD !

반응형

댓글