안녕하세요 후르륵짭잡 입니다.
이번에는 평소에 하고 싶었지만, 두려워서 하지 못 했던 단위테스트 공부한 것을 적어 보려 합니다.
단위 테스트 공부는 아래 유튜브를 통해 좀더 상세하게 알 수 있었습니다.
www.youtube.com/watch?v=P-Zow2yVx4o&t=1481s
그럼 공부 한 것을 정리 해보도록 하겠습니다.
** 단위 테스트! 왜 필요한가? **
사실 단위 테스트가 무엇인지 잘 몰랐습니다.
그냥 테스트 구나 싶었는데, 다른 교육 프로그램의 커리큘럼을 보면 단위 테스트가 있길래
단위 테스트를 공부 해야할 필요성을 느꼈습니다. (머 사실,,,, 뒤쳐질 까봐 공부한거가 맞는거 같아요 ㅎㅎ)
그리고 직접 사용해보니,,,, 정말 필요성을 느꼈습니다.
어떠한 경우를 만들었을 때, 그 부분에 대한 테스트를 해보려면 하나씩 실행 시켜서 테스트 해야하는데,
단위 테스트를 하면 직접 인풋만 주면 오류 검출을 할 수 있기 때문에,
개발하는 차원에서 매우 편리 했습니다.
물론,,,, 코드가 길어진다는 단점이 있지만 정말 필요하다고 생각이 들었습니다.
** 실제로 사용 해보기 **
Unit Test에는 "FIRST" 라는 원칙이 있습니다.
- Fast - 테스팅은 빠르게 동작해야 한다.
- Independent/ Isolated - 독립적이어야 한다.
- Repeatable - 동일한 인풋의 테스팅의 결과는 항상 동일해야 한다.
- Self-Validating - 테스트는 완벽히 자동화되어야 한다. 그 결과 자체로 성공과 실패를 알 수 있어야 한다.
- Timely - 이상적인 테스트 코드는 테스트할 코드를 작성하기 전 작성되어야 한다.
따라서 이 원칙을 지키면서 Unit Test 코드를 작성해야한다는데,,,
(아직 초보자니 일단 시도만 해보았습니다 ㅎㅎㅎ)
- 계산이 정확하게 됐는지 확인 하는 방법 -
enum ErrorHandler : String , Error{
case isEmpty = "데이터가 비어있다."
case overNumber = "숫자가 너무 크다"
case smallNumber = "숫자가 너무 작다"
}
struct Calculater{
func plus(_ input1 : Int? , _ input2 : Int?) throws -> Int{
guard let one = input1 , let two = input2 else {throw ErrorHandler.isEmpty}
let sum = one + two
if sum > 30 {
throw ErrorHandler.overNumber
}
else if sum < 10 {
throw ErrorHandler.smallNumber
}
return one + two
}
}
이 코드를 보면 Enum으로 경우를 나눠 준 것을 확인 할 수 있습니다.
그리고 Error 를 상속 받았습니다.
왜냐하면 Throw를 통해서 오류 검출을 하기 위해서 입니다.
그리고 위에 Caculater 구조체 처럼 작성해주세요.
func plus(_ input1 : Int? , _ input2 : Int?) throws -> Int
이 코드는 반환이 실패하면 오류를 반환하고 그것이 아니면 Int를 반환한다는 것을 의미합니다.
이렇게 작성해주고 이제 Unit Test 코드를 작성해보겠습니다.
import XCTest
//Main 폴더를 import 해줘야 한다.
@testable import UnitTest_Tuto
class UnitTest_TutoTests: XCTestCase {
var test : Calculater!
//This is Like Unit Test ViewDidLoad
override func setUp() {
super.setUp()
test = Calculater()
}
//Clean up all the test
override func tearDown() {
super.tearDown()
test = nil
}
//에러를 반환안하면 Okey
func test_plus_Correct() {
XCTAssertNoThrow(try test.plus(10, 2))
}
//에러를 반환 했을 때 확인 하는 방법
func test_plus_small(){
let errorMessage = ErrorHandler.isEmpty
var error : ErrorHandler?
XCTAssertThrowsError(try test.plus(nil, 10)){ result in
error = result as? ErrorHandler
}
XCTAssertEqual(error, errorMessage)
}
//여러개를 테스트 하는 과정
func test_by_input(){
let errorMessage = ErrorHandler.overNumber
var error : ErrorHandler?
let inputOne : Int? = 10
let inputTwo : Int? = 30
//XCTUnwrap은 nil 값이 있는지 없는지 확인
XCTAssertNoThrow(try XCTUnwrap(inputOne))
//nil인지 아닌지 확인
XCTAssertNotEqual(inputOne, nil)
// XCTAssertEqual(inputTwo, nil)
XCTAssertThrowsError(try test.plus(inputOne, inputTwo)){ result in
error = result as? ErrorHandler
}
XCTAssertEqual(error, errorMessage)
}
}
일단 시작하기 앞서 코드를 작성 할때,
- Given - 입력값을 제공
- When - 입력값을 사용해 테스트의 대상이 되는 메서드를 실행
- Then - 결과 값을 증명(기대괎과 비교)
위와 같이 테스트 코드를 작성 할 수 있도록 습관을 길러야 한다 합니다. (저는 아직 미숙해서 어렵더라구요 ㅠㅠ)
//Main 폴더를 import 해줘야 한다.
@testable import UnitTest_Tuto
일단 Unit Test와 우리의 Project 파일은 서로 다르게 연결 되어 있습니다.
따라서 위의 코드를 적어주면서 접근 허용을 주시기 바랍니다.
class UnitTest_TutoTests: XCTestCase {
var test : Calculater!
//This is Like Unit Test ViewDidLoad
override func setUp() {
super.setUp()
test = Calculater()
}
//Clean up all the test
override func tearDown() {
super.tearDown()
test = nil
}
//에러를 반환안하면 Okey
func test_plus_Correct() {...}
//에러를 반환 했을 때 확인 하는 방법
func test_plus_small(){...}
//여러개를 테스트 하는 과정
func test_by_input(){...}
}
setUp() 함수는 테스트 클래스의 init() 또는 ViewDidLoad와 같은 역할을 합니다.
수행하기 전에 초기화를 해주는 작업 입니다.
반면 tearDown() 함수는 모든 테스트가 끝났을 때, 테스팅 작업을 마치는 작업입니다.
class UnitTest_TutoTests: XCTestCase {
var test : Calculater!
//This is Like Unit Test ViewDidLoad
override func setUp() {...}
//Clean up all the test
override func tearDown() {...}
//에러를 반환안하면 Okey
func test_plus_Correct() {
XCTAssertNoThrow(try test.plus(10, 2))
}
//에러를 반환 했을 때 확인 하는 방법
func test_plus_small(){...}
//여러개를 테스트 하는 과정
func test_by_input(){...}
}
위의 코드를 보면 test_plus_Correct() 함수는 XCTAssertNoThrow() 를 사용하고 있습니다.
XCTAssert...()는 다양한 함수가 존재하는데,
Equel(A,B) : A와 B가 서로 같은지 확인
ThrowsError() : 오류를 반환하는지
등,,,, 여러가지가 존재합니다.
그렇다는 것은 위에 상황을 보면
guard let one = input1 , let two = input2 else {throw ErrorHandler.isEmpty}
let sum = one + two
if sum > 30 {
throw ErrorHandler.overNumber
}
else if sum < 10 {
throw ErrorHandler.smallNumber
}
이렇게 입력 값 중 하나로 없거나 두개의 숫자의 합이 30 이나 10을 넘지 않는다면 오류가 발생하지 않습니다.
따라서 10 과 2를 입력한다면 "Test Success" 가 나올 것 입니다.
만약에, 특정한 오류인지 확인 하려면 어떻게 해야할까요???
//여러개를 테스트 하는 과정
func test_by_input(){
let errorMessage = ErrorHandler.overNumber
var error : ErrorHandler?
let inputOne : Int? = 10
let inputTwo : Int? = 30
//XCTUnwrap은 nil 값이 있는지 없는지 확인
XCTAssertNoThrow(try XCTUnwrap(inputOne))
//nil인지 아닌지 확인
XCTAssertNotEqual(inputOne, nil)
// XCTAssertEqual(inputTwo, nil)
XCTAssertThrowsError(try test.plus(inputOne, inputTwo)){ result in
error = result as? ErrorHandler
}
XCTAssertEqual(error, errorMessage)
}
이렇게 XCTAssertThrowsError를 해서 오류가 발생한다면 Enum 타입인 ErrorHandler 특정 변수(error)에 발생한 오류를 넣어주고
XCTAssertEqual()을 통해서 예측한 오류가 맞는지 확인 할 수 있습니다.
이렇게 Unit Test에 대해 어느정도 알아 봤습니다.
- NetWork 통신 방법 -
실제로 Unit Test를 이용하면 NetWork 체크를 할 수도 있습니다.
struct Info : Codable {
let frameWork : String
let version : Int
}
enum ApiError : String, Error{
case networkInvaild = "네트워크 연결 오류"
case statuseError = "상태 오류"
case decodeError = "해독 중 오류d"
}
class HttpRequest {
func makeUrl(url : String) -> URL? {
let url = URLComponents(string: url)?.url
return url
}
func getInfoData(url : URL? , completeHandler : @escaping (Result<[Info],ApiError>) -> ()) {
guard let url = url else {
completeHandler(.failure(.networkInvaild))
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
excute(request: request, completeHandelr: completeHandler)
}
private func excute(request : URLRequest , completeHandelr : @escaping (Result<[Info],ApiError>) -> ()) {
URLSession.shared.dataTask(with: request) { (data, res, err) in
guard let statusCode = (res as? HTTPURLResponse)?.statusCode else {
completeHandelr(.failure(.statuseError))
return
}
guard (200..<300).contains(statusCode) else {
completeHandelr(.failure(.statuseError))
return}
guard let responseData = data else {
completeHandelr(.failure(.networkInvaild))
return
}
do{
let infoData = try JSONDecoder().decode([Info].self, from: responseData)
completeHandelr(.success(infoData))
}
catch let error {
print(error.localizedDescription)
completeHandelr(.failure(.decodeError))
}
}.resume()
}
}
이렇게 네트워크 통신을 만들고 Vapor를 활용해서 LocalHost 서버를 만들어 줍니다.
그리고 아래와 같이 UnitTest 코드를 작성 해주시고 실행시켜 주면
import XCTest
@testable import UnitTest_Tuto
class HttpReqeustTest: XCTestCase {
var httpRequest : HttpRequest?
override func setUp() {
super.setUp()
httpRequest = HttpRequest()
}
override func tearDown() {
httpRequest = nil
super.tearDown()
}
func test_HttpRequest(){
let expectation = self.expectation(description: "성공적으로 일을 마침")
let url = httpRequest?.makeUrl(url: "http://localhost:8080/hello/vapor")
httpRequest?.getInfoData(url: url, completeHandler: { (result) in
switch result {
case .success(let infos):
XCTAssertNotNil(infos)
expectation.fulfill()
case .failure(let error):
XCTFail(error.rawValue)
}
})
self.waitForExpectations(timeout: 10.0, handler: nil)
}
}
이렇게 초록불이 나오게 됩니다.
반면에 url을 잘 못 적는다면 아래와 같이 빨간 불이 나오면서 오류도 알려주게 됩니다.
사실 아직은 부족한게 많고,,, NetWork 단위 테스트 같은 경우에는 Mock라고 서버가 완료되기전에 만들어 놓을 수도 있습니다,
참고로 expectation은 비동기적은 테스팅을 할 때 사용이 됩니다.
(escaping 클로저랑 비슷하다고 생각하면 될 것 같습니다.)
따라서 성공적으로 fulfill() 함수를 만나면 테스팅을 안전하게 끝마치고
그게 아니라 XCTFail 과 같이 오류를 만난다면 시간안에 종류가 되지 않아 waitForExpectations() 함수가 작동하게 됩니다,
따라서 네트워킹 통신 같은 경우는 이렇게 expectation을 사용해주면 좋을 것 같습니다.
(그리고 위와 같이 Network 작업을 사용하지 않아도 됩니다. 다양한 방법이 있는 것 같습니다.)
소스 코드 :
github.com/HururuekChapChap/Xcode_TestProj/tree/master/UnitTest_Tuto
참고 사이트 :
유닛 테스트란 무엇인가?
좀더 자세한 글
velog.io/@wimes/UnitTest-for-Xcode
expectation에 대한 설명
developer.apple.com/documentation/xctest/xctestcase/1500899-expectation
Mockiing Unit Test
www.youtube.com/watch?v=zB61-7E7eoo&t=189s
'Xcode > Swift - PlayGround' 카테고리의 다른 글
PlayGround) Notification을 이용해서 데이터를 전송하자! (0) | 2020.11.16 |
---|---|
PlayGround) DispatchGroup이란??? (0) | 2020.11.08 |
PlayGround) Delegate와 Delegate Data Pass를 알아보자! (0) | 2020.11.04 |
PlayGround ) Swift로 RC4 알고리즘에 대해 알아보자! (0) | 2020.10.05 |
PlayGround ) Design Pattern (MVC) (0) | 2020.10.02 |
댓글