2021. 4. 3. 17:49ㆍiOS
iOS에서 Unit Test를 무엇일까..?
굳이 왜 Unit Test를 실행해야하고 이러한 것을 함으로서 얻는 이점은 무엇이고 힘든 점은 무엇일까요?
저도 크게 이러한 것에 대해 정확히는 모르고 어림잡아만 알고있었는데요.
이번에 공부할 기회가 되어서 한 번 공부를 해보았습니다.
Unit Test란?
'우선 Unit Test를 왜 하는가?'라는 질문에 답하기 위해서는 Unit Test가 무엇인지를 알아야한다고 생각하는데요.
Unit Test란 크게 어려운 것이 아니고 말그대로 단위 테스트입니다.
개발자들이 어떤 동작을 하는 프로그램을 작성하고 이 프로그램이 작성한 의도대로 동작하는지 검증하는 절차입니다. 이 과정에서 Unit Test를 이용해서 각 모듈들이 잘 동작하는지 확인을 하게 되는데요.
모듈이라 함은 크게는 클래스 작게는 하나의 메소드 정도가 될 수 있습니다.
즉, 특정 모듈이 개발자가 작성한 의도대로 동작하는지 검증하는 절차라고 할 수 있겠습니다.
왜 해야할까요?
근데, 여기서 무슨 생각이 드시나요..?
'저는 왜 굳이 이러한 테스트를 굳이 진행해야하는지'라는 의문이 들었어요.
'내가 작성한 프로그램이라면 당연히 빌드를 하면서 로그도 확인했을거고 맞는 방향으로 동작하는 것을 확인하는 것은 기본일텐데 굳이 일을 늘려서 해야할까..?' 이런 생각말이죠..!
여기에 대해서도 생각을 해보았는데요.
크게 3가지 정도의 이유로 할 수 있겠다고 이유를 뽑았습니다.
우선 첫번째로 Unit Test를 함으로서 각각의 모듈을 부분적으로 확인할 수 있기 때문에, 어떤 모듈에서 문제가 발생하고 있는지를 빠르게 확인할 수 있습니다.
두번째로는 Unit Test가 작성되어 있으면 리팩토링을 하더라도 해당 모듈의 정상 동작 여부를 확인할 수 있기 때문에 변경에 강하다는 장점이 있어요. 즉, 리팩토링을 통해 발생하는 Side Effect에 대해 불안에 떨지 않아도 되고 금방 알 수 있다는 장점이 있습니다.
세번째로는 프로그램이 커질수록 새로운 기능을 확인할 때, 결국 빌드로 확인을 진행하게 될텐데요. 그러한 문제를 Unit 단위로 확인을 진행할 수 있게 되면서 시간 절약 및 빠른 적용이 가능해집니다.
이렇게 제가 생각했을 때, Unit Test의 효용이 어디서 나올지 3가지 정도로 뽑아보았는데요. 사실 너무 간단한 프로그램에 대해서는 당연히 예측하는 범위 내에서 동작하기 때문에 오히려 비효율적이라는 생각도 있을 수 있지만, 프로그램이 커질수록 더욱 효용이 커지고 개발자에게 프로그램이 제대로 동작한다는 하나의 안전장치로 생각할 수 있을 것 같습니다.
어떻게 할까요?
이러한 프로그램을 설계했을 때, 확인하고 싶다는 니즈가 있는 상태입니다.
여기에 대해서 한 번, Unit Test를 작성해보겠습니다.
이렇게 각 모듈에 대해 정확히 동작하는지 각각의 메소드를 작성할 수 있는데요.
우선 2를 배수하는 모듈은 5를 넣었을 때, 10이 정확히 나오는지를 테스트할 수 있습니다.
우선 예상하는 값과 모듈을 통해 나오는 값이 같은지를 확인하고 통과를 함으로서 제대로 동작하는 것을 확인할 수 있습니다.
만약 통과하지 않는다면 잘못 작성된 모듈이게 됩니다.
3을 배수하는 모듈도 역시 예상하는 범위에서 동작하면 통과하는 것을 확인할 수 있고 그렇지 않을 시에는 잘못 작성된 모듈이라고 할 수 있겠는데요.
마지막으로 지수만큼 제곱하는 함수에서는 확인을 진행할 때, 하나의 모듈에 대해서 2가지의 Unit Test 코드를 작성했습니다. 왜냐하면 하나의 동작에 대해서는 당연히 개발자가 예상한대로 동작하는지 확인할 수 있지만 다른 예외 상황에 대해서도 제대로 동작하는지를 확인해야하기 때문이에요.
예로 알고리즘을 풀때에도 보이는 케이스 외에도 통과해야하죠? 그렇기 때문에 개발자가 예상하는 범위 내에서 여러가지 Unit Test를 하는 코드가 작성되어야 합니다.
Unit Test 형식
그리고 이 Unit Test를 함에도 어느정도 템플릿으로 테스트를 진행하는 패턴같은 것이 있는데요.
바로, Given-When-Then 구조입니다.
특별한 것이 있는 것이 아니고 각 단계에서 하는 행동들을 정의해서 어떻게 진행되는지 절차를 정해놓은 것입니다.
Given : 테스트를 준비하기 위해 변수를 초기화하거나 입력하는 단계입니다. 즉, 준비하는 단계입니다.
When : Given 단계에서 준비한 변수를 활용해 실제 액션을 실행하는 단계입니다. 보통 가장 짧은 줄이 작성되는 단계입니다.
Then : When 단계에서 실행된 액션에 대한 결과를 확인하는 단계입니다. 여기가 바로 검증 절차가 들어가는 단계입니다.
사실 이렇게만 하면 감이 잘 안올 수 있는데 정말 어려운 것이 아니기 때문에 한 번 예시를 통해 보도록 하겠습니다~
자판기에 코인을 넣고 음료를 선택하면 음료를 구매하고 만약 코인이 부족하면 음료를 구매할 수 없는 간단한 프로그램입니다
이 코드에 대해서 Unit Test 코드를 작성하고 작성함에 있어서 Given-When-Then 구조를 활용해 보겠습니다.
우선 insert(coin: Int)이라는 메소드에 대해 테스트 코드를 작성하려고 합니다.
func test_inserCoin() {
// given
let coin: Int = 1000
let expectationValue: Int = 1000
// when
VendingMachine.shared.insert(coin: coin)
// then
XCTAssertEqual(VendingMachine.shared.restCoin, expectaionValue, "InsertCoin was failed")
}
우선 넣을 코인을 준비하고 이에 대해 expectationValue로 개발자가 예상하는 답을 입력합니다. 지금 이 단계가 given 단계에 해당하겠죠?
테스트를 위해 준비하는 단계입니다.
다음으로는 when 단계에서 어떤 모듈에 대해 테스트를 진행할 지, 준비한 변수들로 해당 모듈을 실행합니다.
그리고 then 단계에서 when 단계에서 실행한 결과에 대해 검증을 진행하는 단계입니다. 여기서 예상한 결과 값과 모듈의 동작이 맞게 작성되었는지 확인합니다.
여기까지 진행하면 insert(coin: Int)에 대한 테스트가 진행된 것입니다.
다음으로는 pick(drink: Drink)에 대한 테스트를 진행해보겠습니다.
여기서 생각해야할 점은 해당 메소드가 하나의 동작만을 하지않죠?
즉, 성공하는 경우와 실패하는 경우가 존재합니다.
그렇기 때문에, 하나의 테스트 코드가 아닌 모든 경우에 대해 테스트 코드를 작성해야합니다.
우선 성공하는 경우에 대해 작성해 보겠습니다.
func test_pick_drink_Success() {
// given
let coin: Int = 1000
let expectationValue: VendingMachine.Drink = .coffee
VendingMachine.shared.insert(coin: coin)
// when
let pickDrink = VendingMachine.shared.pick(drink: .coffee)
// then
XCTAssertEqual(pickDrink, expectation, "Drink was not Coffee")
}
여기서는 insert(coin: Int)를 하는 것이 준비 단계이기 때문에, given에 들가게 됩니다.
우선 모든 음료를 구매하기 적절한 돈을 넣어주고 when 단계에서 음료를 구매하는 pick(drink: Drink)를 실행합니다.
개발자가 예상하는 값은 뽑은 음료가 나오게 되는것이죠?
then 단계에서 이제 예상한 값이 맞는지 확인합니다.
마지막으로 실패하는 케이스입니다.
func test_pick_drink_failure() {
// given
let coin: Int = 300
VendingMachine.shared.insert(coin: coin)
// when
let pickDrink = VendingMachine.shared.pick(drink: .cola)
// then
XCTAssertNil(pickDrink)
}
이번에는 어떤 음료를 구매하기에도 부족한 돈을 넣고 pickDrink(drink: Drink)를 실행했을 때, 개발자가 예상하는 값은 Nil입니다.
when 단계에서 해당 메소드를 실행하고 then 단계에서 해당 값이 Nil인지 확인하는 절차를 거치면 됩니다.
이렇게 모든 케이스가 통과하는 것을 확인함으로서 해당 모듈이 원하는 대로 동작하는 것을 알 수 있습니다.
비동기 테스트
앞의 경우까지는 동기로 진행되는 것에 대한 테스트여서 크게 어렵지 않았습니다.
그렇다면, 비동기로 진행되는 테스트에 대해서는 어떻게 진행할 수 있을까요..?!
이렇게 테스트케이스를 작성했는데, 당연히 실패하는 테스트 케이스입니다.
URLSession을 이용해서 요청을 보내게되면 비동기로 진행되기 때문에, XCTAsertNotNil이 먼저 실행되어서 이후에 응답이 오기 때문에 실패하게 됩니다.
그렇다면 어떻게 이 경우를 해결할 수 있을까요?
Apple에서 제공해주는 기본 방법이 있습니다!!
func test_async() {
// given
let expectaion = XCTestExpectation(description: "API Request")
let url = URL(string: "https://apple.com")!
let resultData: Data?
let dataTask = URLSession.shared.dataTask(with: url) { data, _, _ in
resultData = data
expectation.fulfill()
}
// when
dataTask.resume()
// then
wait(for: [expecation], timeout: 5)
XCTAssertNotNil(resultData)
}
아까 이전의 코드에서 달라진 부분은 XCTestExpectaion을 활용하는 부분입니다.
이전의 문제는 비동기로 실행되기 때문에, 먼저 끝나버리는 테스트가 문제였습니다.
여기서는 XCTestExpectaion이 fulfill 되기 전에 wait이라는 메소드에서 5초동안 해당 값이 도착하기를 기다리게 됩니다.
즉, 다음 라인으로 넘어가지 않게 됩니다.
그리고 도착했을 때야 비로서야 다음 라인으로 넘어가고 테스트를 진행합니다.
이런식으로 비동기 상황에 대한 테스트도 진행할 수 있습니다.
근데, 이러한 테스트가 방향이 바른 것일까요..?!
만약, 네트워크 연결이 끊긴 상황에서는 이런 테스트는 어떨까요?
무조건 실패하는 테스트가 되어버리겠죠?
사실 이러한 테스트에 대해서도 테스트를 진행할 수 있는 방향이 있는데, 다음 포스트에서 알아보도록 하겠습니다!
마무리
오늘은 Unit Test에 대해 공부하고 방법에 대해 간단하게 작성해보았습니다.
사실 Unit Test에 대해서는 의견이 다양한 주제라 다들 생각이 다를 것 같은데요. 귀찮긴 하지만 해야한다는 의견이 있을수도 있고 굳이 왜해야하냐는 의견이 있을수도 있구요.
Client 개발에서는 UI에 Dependency가 강하기 때문에 테스트를 하기 어려운 부분이 있을수도 있어요. 또한 워낙 기획도 빨리 변화하기 때문에 테스트를 진행하려고 해도 금방 변해서 필요없게 되어버리는 경우도 있을 수 있겠죠.
사실 그렇기 때문에, Unit Test에 대해서는 적용하는 것이 좋다고 생각은 하지만 필요한 부분에만 적용할 수 있도록 고려가 필요한 것 같습니다. (ex 순수하게 비즈니스 로직을 담당하는 부분에서만 적용할 수 있도록 하기)
각 팀에서 팀원들과 협의해서 필요한 부분에서만 적용할 수 있도록 하는 것이 가장 좋은 방법일 것 같습니다.
'iOS' 카테고리의 다른 글
[iOS] 앱에서 Web 보여주기 (WKWebView, SFSafariViewController, Safari 열기) (1) | 2021.04.18 |
---|---|
[iOS] Dependency Injection (의존성 주입) - DI (0) | 2021.04.05 |
[iOS] 커스텀 UIView - xib이용하기 (2가지 방법) (2) | 2021.03.06 |
[iOS] FCM(Firebase Cloud Messaging) + APNs - 푸쉬 알림 등록 (2) (0) | 2021.02.21 |
[iOS] Apple Push Notification Service(APNs) - 푸쉬 알림 등록 (1) (0) | 2021.02.20 |