Unit Test란 무엇인가?

  • 하나의 기능(메소드, 모듈)이 만들어졌을 때, 그 기능이 잘 작동하는지 테스트하는 것이다.
  • 특정 모듈이 의도된 대로 동작하는 지 검증하는 절차이며 모든 함수와 메소드에 대한 테스트이다.

Unit Test를 왜 해야하나?

  • 코드의 신뢰성을 보장할 수 있다.
  • 새로운 코드를 반영하기에 유리하다.
  • 기능 명세서의 역할도 수행할 수 있다.
  • 어느 부분이 문제인지 쉽게 파악할 수 있다.

어떻게 신뢰성을 보장할 수 있는데?

개발 중에 어떤 스펙이 변경되서 코드를 수정했다고 생각해보자. 코드를 수정했을 때에도 최초에 요구했던(만족했던) 테스트 결과가 수정 후에도 잘 동작하는지 검증할 수 있다.

왜 새로운 코드를 반영하기에 유리한데?

신뢰성 보장과 비슷한 맥락이다. 어떤 코드를 추가하고 수정하더라도 작성해둔 테스트들을 돌려보면 "내가 수정하고 추가한 부분이 잘 돌아가는구나" , "기존 코드에 나쁜 영향을 미치지 않았구나"와 같이 새로운 코드를 반영했을 때 어떤 영향이 미치는지 쉽게 알 수 있다.

기능명세서?

프로젝트 진행 중에 새로운 개발자가 입사했다 생각해보자. 코드를 처음 보는 개발자는 기능들의 테스트 케이스들을 보고 "이 메소드는 이런 이런 동작을 하는구나" 하고 쉽게 파악할 수 있다.

조금 더 쉽게 예시를 들어보자.

핸드폰을 구매하게되면 충전기를 무료로 준다. 이 충전기는 불과 몇 년 전만 해도 케이블과 어댑터가 일체형으로 제공되었는데, 충전에 문제가 발생했을 경우 어디가 문제인지 쉽게 알 수 없었다.

하지만 요즘에는 케이블과 어댑터가 분리되어서 나온다. 그렇기 때문에 충전에 문제가 생겼을 때
"아 케이블에 문제가 생겼구나" 또는 "아 어댑터에 문제가 생겼구나"하고 어디에 문제가 생겼는지 쉽게 파악할 수 있다.

만약 핸드폰이랑 케이블이랑 어댑터가 일체형이라면... 어디 부분에 문제가 발생했는 지 파악하기 어렵다.

마찬가지로 Unit Test는 기능 단위로 테스트하기 때문에 기능 중에 어느 부분이 문제가 발생했는지를 쉽게 파악할 수 있다.


그래서 어떤 테스트를 작성해야하나?

테스트를 작성하는데 있어서 가이드가 되는 원칙이 있다.
수많은 테스트 방법론에서 잘 알려진 원칙 중에 하나인 FIRST원칙을 알아보자.
참고로 FIRST원칙 외에 다양한 기준과 방법이 존재하므로 너무 맹신하면 안된다.

Fast

테스트는 빠르게 동작해야한다.
하나의 메소드에도 굉장히 많은 테스트가 작성된다. 만약에 테스트가 느리게 동작한다면 테스트를 실행하는 데에만 몇 분의 시간을 소요할 수 있다. 그렇기 때문에 빠르게 동작해야한다.

Independent

Unit Test는 메소드 단위로 테스트를 작성하는데 각각의 테스트들은 독립적이여야한다. 서로 의존관계가 있다면 특정 테스트가 다른 특정 테스트에 영향을 미칠 수 있기 때문에 의도한대로 테스트를 진행할 수 없는 경우가 발생하기 때문이다.

Repeatable

테스트는 수천수만 번을 하던, 언제 어디서 수행을 하던, 같은 결과로 반복이 되어야 한다.

Self-Validating

테스트는 테스트코드 내부에서 테스트가 잘 동작하는지를 판단해야 한다. 다시 말해 테스트 케이스에서 테스트를 완료해야 한다는 의미이다.

Timely

테스트를 언제 작성하느냐에 있어서 정답은 없다. 기능을 구현하기 전에 작성하는 것이 이상적이지만 현실적으로 쉽지 않은 부분이다.
기능을 구현한 후에 테스트를 할 경우 그 기능에 대해서 테스트하기 곤란한 경우가 발생할 수 있다. 기능 구현을 다 해놨는데 테스트가 안되서 테스트를 위해 코드를 수정해야하는 상황이 발생할 수 있다.
반대로, 기능을 구현하기 전에 테스트를 작성할 경우 테스트가 가능한 메소드를 만들 수 있게 된다.


테스트코드를 작성해보자.

먼저 프로젝트 만들 때 테스트를 포함하도록 체크해준다.

<프로젝트 이름>Tests.swift 파일에 가보면 이런 메소드를 볼 수 있다.

	override func setUpWithError() throws {

    }

    override func tearDownWithError() throws {
    
    }

    func testExample() throws {
        
    }

    func testPerformanceExample() throws {
    	self.measure {
        
        }
    }

setUpWithError() : 테스트 케이스가 실행될 때 호출되는 메소드
tearDownWithError() : 테스트 케이스가 실행되고 종료될 때 호출되는 메소드
testExample() : 테스트할 코드를 적는 메소드
testPerformanceExample(): 기능에 대한 성능을 테스트 하는 메소드

테스트를 진행하게되면 setUpWithError()가 실행되고 testExample() 메소드로 테스트를 하며, 종료될 때 tearDownWithError()가 실행된다.

setUpWithError()tearDownWithError()는 왜 존재할까?

특정 기능에 대해 테스트1, 2가 있다고 생각해보자.
테스트 1에서 특정 모듈이나 클래스의 프로퍼티를 변경했을때 테스트 2에서도 변경된 프로퍼티가 적용되어 있다. 이는 FIRST 원칙중에 독립적이여야한다는 원칙 위반하므로 독립적인 테스트를 위해 존재한다.

테스트할 기능을 구현한 메소드

	struct LottoMachine {
    	func isValidLottoNumbers(of numbers: [Int]) -> Bool {
        	guard numbers.count == 6, Set(numbers).count == 6 else {
            	return false
        	}
        
        	for num in numbers {
            	guard 1...45 ~= num else {
                	return false
            	}
        	}
        
        	return true
    	}
	}

테스트 코드

테스트 코드는 기능명세서의 역할도 한다했는데 이를 좀 더 명확하게 하기 위해 한글로 작성되기도 한다.
또한 테스트 코드 메소드 이름은 prefix로 "test"가 고정된다.

	class LottoMachineTests: XCTestCase {
    	let lottoMachine = LottoMachine()
    
    	func test_6개보다_적은숫자를_입력하면_false() {
        	// given
        	let input = [5, 7, 10, 32]
        
        	// when
        	let result = lottoMachine.isValidLottoNumbers(of: input)
            
        	// then
        	XCTAssertFalse(result)
    	}
	}

테스크 코드를 작성하면 메소드 왼쪽에 버튼이 하나 생기는 데 그걸 gutter 라고 하며 gutter를 누르게 되면 테스트가 실행된다.

테스트가 성공했다면(잘 작동했다면) 초록색으로 변할 것이고, 실패했다면 빨간색으로 표시된다.

profile
iOS 개발자가 되고싶어요

0개의 댓글