[ Unit Test ] Xcode 에서 단위 테스트 하는 것, XCTest - 1

2024. 1. 18. 02:27Mobile/iOS

팀프로젝트 파일 내 포함된 코드나 다른 사람들의 코드를 보면 꽤 테스트 코드를 설정해준 프로젝트들이 많았다.  예전부터 TDD(Test Driven Development - 테스트 주도 개발) 라고 자주 들어봤는데, 어떻게 Xcode 에서 적용해볼지 막막했다. 마침 클린코드 책을 읽으면서 단위테스트라는 챕터를 읽었다. 테스트는 왜 하는지에 대한 의문을 가졌다. 코드의 유지보수성, 재사용성, 유연성이 프로덕트를 보존하고 강화할 수 있어서 라는데 사실 개발을 공부하는 초보 입장에서는 와닿지 않는다. 


테스트 코드를 작성한다의 의미 

  • 무엇을 구현할 지 알고 있다.
  • 어떤 기능인지 명확하게 알고 있다.
  • 요구 사항을 파악하고 있다. 

( 22년도 12월쯤 야곰의 원티드 프리온보딩에서 TDD 특강에 대해 들었던 적이 있었다. 노션에서 Test 관련 검색을 하다가 발견,, 기록이 중요하다고 새삼 느낀다. 거기서 알려준 테스트 코드 작성의 정의 ) 


테스트에 대한 학습을 이론에서부터 살펴보고, 실전 코드 짜는 방법으로 나눠서 다음 글에서 작성해 볼 예정이다. 

 

Swift - Xcode 에서 XCTest 하는 방법

Xcode 에서 XCTest 해 본 결과 ( 튜토리얼은 코데코의 Unit Testing 을 참고하여 코드를 작성하고 실행해보았다. )

다음 글에서 실질적인 테스트 파일 작성 및 코드 작성과 XCTest 의 기본적인 내용은 작성해 볼 예정이다. 

위의 두 사진과 같이 코드에서 테스트 파일을 추가하고 관련 코드를 짜고 실행하고, 성능치를 확인하는 방법은 대략 이러하다.

 

테스트 모범 사례는 클린코드에서도 언급했던 F.I.R.S.T 법칙이다.  (Kodeco 튜토리얼에서도 언급)

더보기

FIRST 라는 약어 → 효과적 단위테스트를 위한 간결한 기준 세트를 설명함

  • Fast : 테스트는 빠르게 실행 되어야함.
  • Independent / Isolated(독립/격리): 서로 상태를 공유하지 않아야 함.
  • Repeatable(반복 가능):  테스트를 실행할 때마다 동일한 결과를 얻어야 함. 외부 데이터 공급자 또는 동시성 문제로 인해 간헐적인 실패가 발생할 수 있음.
  • Self-validating(자체 검증): 테스트는 완전 자동화되어야 함. 로그 파일에 대한 프로그래머의 해석에 의존하지 않고 ‘합격’ 또는 ‘불합격’ 중 하나를 출력해야 함.
  • Timely(시기 적절성): 테스트를 테스트하는 프로덕션 코드를 작성하기 전 테스트를 작성하는 것이 이상적. ⇒ 이를 테스트 중심 개발이라고 함(Test-driven development)

앞서 XCTest 에 대해 직접 내가 구현해보기 전, 외국 개발자가 쓴 블로그 글을 참조해 그 글을 읽어 보았다. 그는 XCTest 를 하면서 유의할 점과 개발자들이 간과할 수 있는 부분에 대해 주관적인 생각과 객관적인 지표를 언급했길래 글을 읽어보고 대략적으로 정리해봤다. 

대부분의 개발자가 짠 XCTest 는 잘못되었다.

(꽤나 직설적이고 Critic한(비판적인) 제목인 것 같긴하다.)

https://qualitycoding.org/xctestcase-teardown/

 

When is an XCTestCase Deallocated? - 언제 XCTestCase 를 할당 해제 하나요? 

XCTestCase 는 언제 할당 해제되나? 

Objective-C 가 수동 메모리 관리에서 벗어나면서 더 이상 오브젝트를 릴리즈할 필요가 없어졌다. ARC 는 마법과도 같다. retain Cycle(유지 주기) 을 제외하고는 오브젝트가 저절로 사라진다고 생각하는데 익숙해졌다.

그렇다면 Swift 로 testcase 를 작성할 때 생각해 볼 수 있는 논점은 무엇일까?

 

내 의견:
할당 해제는 메모리 공간의 한정적인 사용을 유동적으로 관리해주기 위해 할당하고, 다 사용한 메모리는 해제시켜서 관리를 해주긴 해야한다. 테스트에서도 그 부분이 중요하긴 할 것 같다. 테스트를 하면서 발생되는 메모리들은 실제 기능에 쓰이는 게 아니라 기능이나 UI Test 를 하기 위한 목적이니 말이다.

 

왜 setUp 과 tearDown에 신경써야 하나? - 설치와 해제

왜 귀찮을까? 예를 들어, 테스트 픽스쳐를 만들때, 테스트 중인 시스템을 만드는 이 방식에 어떤 문제가 있을까?

class ThingTests: XCTestCase {
	private let sut = Thing()
	testSomethingOnSUT() { ... }
	testSomethingElseOnSUT() { ... }
}

setUp 과 tearDown 여전히 중요하다 - 설치, 해제

글쓴이 setUp 에서 테스트중인 시스템을 생성하고 tearDown 에서 릴리스

class ThingTests: XCTestCase {
	private let sut = Thing!
	override func setUp() {
		super.setUp()
		sut = Thing()
	}
	override func tearDown() {
		sut = nil 
		super.tearDown()
	}
	testSomethingOnSUT() { ... }
	testSomethingElseOnSUT() { ... }
}

왜일까? 객체 수명 주기 문제의 보고를 개선하고 싶었다. 그래서 xUnit 프레임워크를 작성한 적이 있다. 제어 흐름이 이런식으로 진행된다는 것을 글쓴이는 알고 있었다.

  1. 예외를 포착하려고 시도하는 래핑
  2. setUp 호출
  3. test method 실행
  4. assertion 이 충족되지 않으면 테스트 오류를 보고한다
  5. tearDown 호출
  6. catch 를 사용해 예외를 포착하고 테스트 실패로 보고

SUT (System Under Test 의 약자) 의 생성과 소멸이 시도 포착 범위에 포함되기를 원한다. 이렇게 하면 예외가 발생하면 XCTest 가 이를 특정 테스트의 실패로 보고한다. 충돌이 발생하더라도 테스트 로그에 어떤 테스트가 시작되었지만 완료되지 않았는지 표시할 수 있음

정말, XCTest 는 언제 할당 해제가 될까??

잠깐, 뭐라고? XCTestCase 가 실제로 할당해제된 적이 없다. XCTestCase 는 언제 할당되나?

xUnit 이 작동하는 방법

xUnit 프레임워크는 공동 아키텍쳐를 공유한다.

  • 시험은 세트로 구성
  • 개별 테스트를 수동으로 생성 이를 모음으로 그룹화하는 방법이 있을 수 있으나, TestCase 클래스를 사용하는 것이 더 쉬운 방법임.
  • 이 클래스에서는 테스트로서 어떤식으로든 주석이 달린 메서드가 포함되어 있고, 각 테스트 메서드는 테스트케이스에 해당하는 Suite 내 단일 테스트가 된다.
NOTE: xUnit 
다양한 코드중심 테스트 프레임워크는 xUnit 으로 통칭되는 테스트 프레임워크를 가지고 있다. 이러한 프레임워크는 켄트 벡(Kent Beck) 이 고안해 낸 것으로, SUnit 이라는 이름으로 Smalltalk 에 처음 적용되었다. 
xUnit 의 구조
- 테스트 러너
- 테스트 케이스
- 테스트 픽스처
- 테스트 스위트(Suite)
- 테스트 실행
- 테스트 결과 포매터 
- 어설션  

 

XCTest 는 런타임에 XCTestCase 의 모든 서브클래스에 대해 쿼리한다. 이러한 각 클래스에 대해 다음과 같은

그 메서드는

  • private 하게(접근제어자) 선언되지 않는다
  • 접두사 test 로 시작하는 이름이 있음
  • 매개변수가 없음
  • 반환값이 없는 경우들

⇒ 모든 메서드를 쿼리함.

잘못된 가정

  • 특정 XCTestCase 서브클래스가 인스턴스화되었음.
  • 특정 테스트 메서드가 호출됨 (try - catch 범위에서)
  • XCTestCase 가 소멸됨.

실제로 일어나는 것

  • XCTest는 런타임에 XCTestCase/테스트 메서드 조합을 쿼리
  • 각 조합에 대해:
    • 특정 테스트 메서드와 연결된 새 테스트 케이스가 인스턴스화된다.
    • 이들은 모두 컬렉션으로 집계된다.
    • XCTest는 이 테스트 케이스 컬렉션을 반복한다.

⛔️ Danger Zone

setUp(설정) 및 tearDown(해제) 기능은 테스트케이스의 전체 컬렉션이 미리 생성되기 때문에 개발됨. 테스트에서 객체 수명 주기를 관리할 수 있는 hooks 을 제공함

 

여기서 두 가지 중요한 점

  1. XCTestCase 초기화 일부로 자동 생성된 모든 것이 너무 빨리 존재하게 됨
  2. 다른 테스트가 실행되는 동안에도 tearDown 에서 해제되지 않은 모든 것이 계속 존재

Global 상태를 변경하는 것이 있다면,,,(좋지 못한 듯..) 객체가 생성될 때 메서드 스위즐링을 수행하는 객체가 있다고 가정해보자. 이 객체는 메서드가 할당 해제되면 원래 메서드를 복원한다. 이 객체를 설정에서 생성했지만 해제하지 않았다면 이 객체는 계속 존재하게 된다. 따라서 한 테스트에 대해 수행된 메서드 스위즐링은 실수로 나머지 모든 테스트에 영향을 미친다.

NOTE: 메서드 스위즐링이란 (Method Swizzling) 
런타임 시점에 특정 메서드를 다른 메서드로 바꾸어 실행하는 기능. 런타임은 프로그램을 실행하는 시점을 말한다. 

글쓴이의 결론

경험의 법칙으로 요약

setUp셋업에서 생성한 모든 오브젝트는 tearDown티어다운에서 소멸되어야 한다.

테스트 케이스 속성이 들어가지 않도록 한다. 설정 시 값이 할당되는 암시적으로 언래핑된 변수를 사용하면 됩니다. 또는 computed property를 사용한다.


튜토리얼을 보고 관련 테스트 코드도 실행해보고, Unit Test Class 나 UI Test Class 도 만들어봤다. 대략적으로 이렇게 실행하는구나. CPU 에서의 활성화 속도, 성능 등을 측정하는구나 라는 것을 알게 되었다. 앞에서 언급한 바와 같이 테스트 튜토리얼과 메소드에 대한 설명을 이 글에 다 작성하게 되면 흐름도 길어질 것 같아서, 마무리를 해야할 것 같다. 

    앞으로, 나는 XCTest 에서 작성할 수 있는 코드들을 학습해보고 실행해보고, 테스트 파일 생성과 테스트 코드 작성 및 실행에 대한 내용을 정리해 XCTest 2 에서 작성을 해보려고 한다. 결국, 테스트는 실패할 가능성에 대해서도 전제를 깔고 고의적으로 실패도 내보고 성능을 측정하면서 앱 구동에 대해 테스트를 해보는 것 같다. 

 

참고 문서

Most Swift Devs Are Wrong About XCTestCase tearDown…

https://www.kodeco.com/21020457-ios-unit-testing-and-ui-testing-tutorial?page=3

https://ko.wikipedia.org/wiki/%ED%85%8C%EC%8A%A4%ED%8A%B8_%EC%A3%BC%EB%8F%84_%EA%B0%9C%EB%B0%9C