iOS Testing - UnitTest편

iOS Unit & UI 테스트

인턴업무 중 Android UnitTest와 UI Test(단위 테스트)를 맡아 진행했습니다. Android 의 경우 Google 에서 제공하는 UIAutomator를 사용하여 테스트 스크립트를 직접 구현해서 테스트 했습니다. Test Orchestrator 등 강제종료에 대처하는 방법등을 연구하면서 업무를 했습니다. 과연 iOS 의 단위테스트와 UI 테스트는 어떻게 수행하는지 정리해보고 개인프로젝트 ARDust 에 적용해보도록 하겠습니다.

시작하기 전에

FIRST : 테스트에 대한 모범사례 (Best Practices for Testing)

두문자어 FIRST는 단위 테스트를 위한 간결한 기준(criteria) 세트를 효과적으로 설명한다. 해당 기준은 다음과 같다.

  • Fast(빠르게) : 테스트는 빨리 실행되야 한다. 그래서 사람들이 신경쓰지 않는다.
  • Independent/Isolated(독립적/분리된) : 테스트는 따로 설정이나 분리를 해서는 안된다.
  • Repeatable(반복가능한) : 테스트 수행할때마다 동일한 결과를 얻어야 한다. 외부의 데이터 공급자나 동시성(concurrency) 문제로 인해 일시적으로 오류가 발생 할수 있다.
  • Self-validating(자체 검증) : 테스트는 완전히 자동화 되어야 한다. 로그 파일에 대한 프로그래머의 해석보다는 패스(pass)또는 실패(fail)를 출력해야 한다.
  • Timely(적시에) : 이상적인 테스트는 테스트한 생산 코드를 작성하기 전에 작성해야 한다.

출처 : https://kka7.tistory.com/68?category=975272

Unit Test 란?

컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는 지 검증하는 절차

즉, 모든 함수와 메소드에 대한 테스트케이스(Test Case)를 작성하는 절차를 말한다. 이를 통해서 언제라도 코드 변경으로 인해 문제가 발생할 경우 단시간 내에 이를 파악하고 바로 잡을 수 있도록 해준다.

이상적으로, 각 테스트 케이스는 서로 분리되어야 한다. 이를 가짜 객체(Mock Object) 를 생성하는 것도 좋은 방법이다.

사실 테스트 업무는 서비스 이전, 이후 에도 매우 중요하기 때문에 회사나 팀에서 SQE 검증 팀이 있어서 지속적으로 개발팀에 피드백을 해주는 구조이다. 하지만 개발자도 TDD(Test Driven Development) 등 개발중에 테스트를 주입하여 사전에 작은 문제를 발견하고 해결하기 위해 테스팅의 개념을 잘 알고 있어야한다.

Xcode의 Test 사전준비

프로젝트 생성시 Unit Test 와 Ui Test를 할 수 있게 프로젝트에서 영역이 구분 되어있다.

image

iOS UnitTest

기본적으로 내가 테스트 하고자 하는 프로젝트의 이름뒤에 Tests 가 붙여진 ARDustTests 파일이 생성이 된다. 그리고 ARDustTests.swift 파일을 보면 아래와 같이 4개의 메서드가 존재한다.

import XCTest
// ARDust의 클래스와 메서드에 접근할 수 있게 해준다. 
@testable import ARDust

class ARDustTests: XCTestCase {
		// 1
    override func setUp() {
    }
		// 2
    override func tearDown() {
    }
		// 3
    func testExample() {   
    }
		// 4
    func testPerformanceExample() {
        self.measure {       
        }
    }

}

XCTest 를 import 하고 있고 ARDustTests를 XCTestCase 의 하하위클래스로 정의하고 있으며 setup() , tearDown() 와 테스트 메서드를 정의한다.

Test Class 를 실행하는 3가지 방법

  1. Product\Test 또는 command-U, 작성한 전체 테스트케이스 클래스를 실행
  2. 테스트 네비게이터에서 화살표 버튼을 클릭

image

  1. gutter 에서 다이아몬드 버튼을 클릭하여 내가 수행하고 싶은 메서드영역을 테스트한다.

image

위의 방법을 사용해서 테스트가 성공하면 테스트 영역에 녹색 체크표시가 나타나고 testPerformanceExample() 끝에 있는 회색 다이아몬드를 클릭하면 성능 결과를 연다.

image

XCTAssert를 사용하여 모델 테스트

우선 ARDust 앱의 모델의 핵심 기능을 XCTAsset를 이용하여 테스트 한다. 간단하게 테스트 swift 파일을 생성해서 테스트 해보겠다.

아래와 같이 우선 유닛테스트가 ARDust의 클래스와 메서드에 접근할 수 있도록 @testable import 를 해준다.

@testable import ARDust

그리고 테스트하고자 하는 클래스의 객체를 선언한다.

class DustMeasure {
    var time = 0
    var totalDust = 0   // 초기 미세먼지 양
    var ultraDust = 0 // 초미세먼지 양
    
    // 측정 시작
    func startMeasure() {
        time = 0
      	totalDust = 50
    }
    // dust 정보 가져온다 가정
    func addUltraDust(_ dust: Int) {
        time = time + 1 // 시간은 늘어나고
        ultraDust += dust // 기존의 초미세먼지에 추가적으로 더한다.
        totalDust += ultraDust // 초기미세먼지양에 초미세먼지의 양을 더한다.
    }
    
    func check(guess: Int) -> Int {
      	// 추측값과 실제 값의 차이를 반환한다.
        return totalDust - guess
    }
}

Test를 위해 위의 클래스를 추가했다. 간단하게 미세먼지를 측정하는 메서드도 추가했다.

addUltraDust 메서드 에서 측정한 시간을 늘리고 기존의 초미세먼지(ultraDust)의 양에 매개변수로 입력 받은 값만큼 늘려주고 총미세먼지 양(totalDust) 의 값도 늘려준다.

import XCTest
@testable import ARDust

class ARDustTests: XCTestCase {
    // 1. SUT(System Under Test)
    var dustUnderTest: DustMeasure!
    // 2.
    override func setUp() {
        super.setUp()
      	// SUT 객체 생성과 동작하는 코드 기입
        dustUnderTest = DustMeasure()
        dustUnderTest.startMeasure()
    		// 초미세먼지 추가 + 30
      	dustUnderTest.addUltraDust(30)
    }
    // 3.
    override func tearDown() {
        super.tearDown()
       	// SUT 객체 해제
        dustUnderTest = nil
    }
    // 4.
    func testScoreIsComputed() {
        // given
        let guess = dustUnderTest.totalDust - 10	// error를 발생시키기 위한 임의의 추측값
        // when
        let difference = dustUnderTest.check(guess: guess)
        // then
        XCTAssertEqual(difference, 0, "차이가 0이 아니므로, 예측한 미세먼지 총량이 다릅니다.")
        
    }
    
    func testPerformanceExample() {
        self.measure {
        }
    }
    
}

그러면 Test 코드를 작성해보자.

  1. 우선 아까 생성한 클래스를 클래스 수준에서 SUT(System Under Test) 객체를 선언 한다.

  2. setUp : 이 메서안에서 선언한 SUT 객체를 생성한다. 이 메서드에서드는 이 테스트 클래스 에서 진행되는 모든 테스트케이스에서 공통으로 필요로 하는 행위들을 정의한다. 즉 클래스 안에 속하는 테스트 메서드들이 호출되기 전 항상 setUp 메서드가 호출된다.

  3. tearDown : 이 메서드 안에서 생성한 SUT 객체를 해제한다. setUp 과 반대로 모든 테스트 케이스 메서드들이 종료된 후 호출되는 메서드로 setUp 에서 생성되고 테스트메서드에서 사용된 것들을 정리해주고 해제할 필요가 있을때 해당 내용들을 이 메서드 안에서 해주면 된다.

  4. 테스트메서드의 경우 test 접두어를 붙인 메서드로 자동으로 test 접두어를 넣을 시 메서드가 testable 하게 된다. (gutter 에 다이아몬드 버튼 생성) 그리고 반드시 각 테스트 케이스는 하나의 메서드로 분리하여 작성해야한다. 테스트 메서드도 메서드로 하나의 기능만을 수행해야하기 때문에 하나의 테스트 메서드 안에 여러 케이스를 넣는 것은 올바른 방법이 아니다.

    그리고 테스트 메서드는 위의 testScoreIsComputed 처럼 하나의 형식을 따르는 것이 좋다.

test메서드의 형식 : given, when ,then

테스트 메서드는 반드시 하나의 테스트케이스만 존재하게 분리하여 작성해야한다. 그렇기 때문에 이런 규칙 하에 특정한 형식을 따르는 것이 바람직 하다. 그 형식은 3단계 given, when, then 으로 나누어져있다.

  1. given : 필요한 모든 값을 설정한다. 예측할 guess 설정한다. totalDust 값을 guess 값으로 설정했다.
  2. when : 테스트 중인 코드를 실행한다. dustUnderTest.check(guess: guess) 를 호출하여 값의 차이를 가져온다.
  3. then : 예상한 결과를 확인한다. 만약 예측한 값(guess) 와 결과값 (totalDust) 가 다르다면 check 함수에서 두 값의 차이를 반환하는데 같지 않으므로 차이가 0 이 반환되지 않고 결국 실패 할 것이다. 만약 아래 -10 을 뺀다면 정상적으로 테스트는 종료될 것이다.

image

오늘은 iOS의 Unit Test를 Xcode에서 어떻게 수행하는지에 대해서 공부해 보았습니다. 우선 iOS에서도 Android JUnit/UI Automator 와 마찬가지로 UnitTest를 할 수 있게 XCTest 라이브러리를 제공하고 있습니다. 테스팅에 대한 간단한 방법등을 익혔는데 앞으로는 보다 효율적이고 좋은 테스팅은 어떤 것인지에 대해서 찾아보고 몇개의 시리즈로 나누어서 실제 현업에서는 어떻게 TDD를 통해 테스트와 개발을 병행하는지에 대해서 기록하도록 하겠습니다


Reference





© 2020. by Gaki

Powered by gaki