Apr 2, 2021, TIL (Today I Learned) - 유닛 테스트

Inwoo Hwang·2021년 8월 26일
0
post-thumbnail

학습내용


계산기기능을 구현만 한 뒤 앱으로 만들면 어떻게 될까? 내가 생각하지 못한 부분에서 에러가 나면 다시 처음부터 하나하나 고친 뒤 다시 해당 앱을 업로드 해야 할 것이다. 해결하기 전에 해당 앱을 다운로드 받은 유저는 문제를 해결한 버전을 다시 다운로드 받으면 되긴하지만 그 전에 개발자에 대한 신뢰를 잃지 않을까?

이런 불상사를 막기 위해 기능이 잘 작동하는지 확인이 필요하다. 이를 위해 존재하는 것이 바로

Unit Test이다.

유닛 테스트 초기 세팅하기

유닛 테스트 하기 위에서는 아래와 같은 세팅이 필요하다.

Screen Shot 2021-04-02 at 2 10 54 PM

테스트에 필요한 프로젝트의 Targets에 [우측]Unit Testing Bundle을 더 해주면 테스트파일이 생기고 유닛테스트 준비가 끝난다.

유닛 테스트 하기!

import XCTest
@testable import 테스트할 프로젝트 이름

XcodeTest를 import해야하는 것 뿐 아니라 테스트할 프로젝트 또한 import 해 와야 한다.

테스트할 앱은 우리가 만든 프로젝트와 다른 타겟설정이 되어있기 때문에...

class CalculatorTests: XCTestCase {
    private var sut_inputDataValidator: InputDataValidator!
    private var sut_calculator: GeneralCalculator!
    private var sut_decimalCalculation: DecimalCalculation!
    private var sut_binaryCalcualtion: BinaryCalculation!

sut 또는 "subject under test"를 테스트 케이스의 변수로 생성을 한뒤 테스트 하려는 클래스로 선언 해 주면 해당 클래스의 인스턴스 생성을 할 수가 있어진다.

저 같은 경우 각각의 클래스에서 필요한 메서드를 사용하기 위해 이렇게 총 4개의 계산기 클래스 타입의 변수를 생성하였습니다.

override func setUpWithError() throws {
  sut_inputDataValidator = InputDataValidator()
  sut_generalCalculator = GeneralCalculator()
  sut_decimalCalculation = DecimalCalculation()
  sut_binaryCalcualtion = BinaryCalculation()

  try super.setUpWithError()
}

setUpWithError 메서드는 테스트 케이스가 시작 될 때 첫 번째 테스트가 실행되기 전에 한 번 호출 되는 메서드 입니다.

항목이 존재하거나 특정 상태가 필요한 경우 위 과정은 필수로 해야 합니다.

이번 프로젝트 같은 경우 각각 의 클래스에서 메서드를 가져와 테스트 해야 하기 때문에 인스턴스와 같이 이렇게 설정을 해주었습니다.

override func tearDownWithError() throws {
        try super.tearDownWithError()
        sut_inputDataValidator = nil
        sut_generalCalculator = nil
        sut_binaryCalcualtion = nil
        sut_decimalCalculation = nil
    }

tearDownWithError 메서드는 모든 개별 테스트가 실행 된 후 끝날 때 정확히 한 번 호출됩니다.

이번 테스트 같은 경우 테스트가 끝난 뒤 연산이 끝 난 데이터를 지워주어야 하기 때문에 테스트가 끝난 뒤 모든 값이 nil로 되도록 설정 해주었습니다.

func test_elements_of_medianNotation() throws {
  sut_inputDataValidator.validateData(input: "0")
  sut_inputDataValidator.validateData(input: "1")
  sut_inputDataValidator.validateData(input: "0")
  sut_inputDataValidator.validateData(input: "1")
  sut_inputDataValidator.validateData(input: "+")
  sut_inputDataValidator.validateData(input: "1")
  sut_inputDataValidator.validateData(input: "1")
  sut_inputDataValidator.validateData(input: "1")
  sut_inputDataValidator.validateData(input: "1")
  XCTAssertEqual(sut_inputDataValidator.data.medianNotation, ["0101","+", "1111"])
    }

연산자와 피연산자를 inputDataValidator.data.medianNotation에 넣어 준 뒤 해당 데이터가 잘 들어 있는지 확인하는 테스트를 진행하였습니다.

XctAssertEqual() 메서드를 활용하여 데이터에 들어있는 값이 해당 인자값 왼쪽에 있는 값과 동일한지 테스트할 수 있습니다.

⚠️ 여기서 주의해야 할 점은 메서드 이름을 꼭 test로 시작 해 줘야 한다는 것 입니다.

이렇게 설정 해 주지 않으면 테스트를 할 수 없습니다. 주의 해 주세요!!

Return

enum Error: Swift.Error {
  case invalidAccess
  case invalidOperation
}
  @discardableResult
  func calculatePostfixNotation(_ input: InputDataValidator) -> Result <String, Error> {
    var operandStack = Stack<Double>()

    for element in input.data.postfixNotation {
      if !Operators.list.contains(element) {
        guard let numbers = Double(element) else {
          return .failure(.invalidAccess)
        }

        operandStack.push(numbers)
      }
      else {
        guard let firstPoppedValue = operandStack.pop(),
        let secondPoppedValue = operandStack.pop() else {
          return .failure(.invalidAccess)
        }

        rightOperand = firstPoppedValue.value
        leftOperand = secondPoppedValue.value

        switch element {
          case "*" :
          operandStack.push(leftOperand * rightOperand)
          case "/" :
          operandStack.push(leftOperand / rightOperand)
          case "+" :
          operandStack.push(leftOperand + rightOperand)
          case "-" :
          operandStack.push(leftOperand - rightOperand)
          default:
          return .failure(.invalidOperation)
        }
      }
    }
    guard let peek = operandStack.peek() else {
      return .failure(.invalidAccess)
    }
    return .success((dropDigits(peek.value)))
  }

@discardableResult : "결과를 쓰든 안쓰든 신경 쓰지마세요. unused Warning 띄우지 마세요"

결과를 사용하지 않긴 하지만 테스트를 위해서 해당 메서드의 반환 값을 명시 해 줘야 할 때 이걸 사용하면 됩니다.

그리고 위 메서드의 리턴 값을 Result<String, Error> 타입으로 설정 해 줬습니다.

이렇게 사용하면 유닛 테스트를 할 때 .failure() 또는 .success() 메서드를 리턴 해 줄 수 있습니다. 이렇게하면 유닛테스트를 진행할 때 에러처리도 가능하기에 테스트할 때 유용한 기능인 것 같습니다.

위와 같은 방법으로 하나 하나 메서드를 실행햐여 Unit Test를 진행할 수 있습니다.

Code Coverage

프로젝트 내에 불필요한 코드가 없는지 또는 코드가 모두 잘 사용되었는지 확인하기 위해서 Code Coverage를 구현 하는 게 좋습니다. 자세한 방법은 아래 블로그를 참조하면 됩니다.↓↓

Code Coverage (tistory.com)

고민한 내용/ 해결한 부분


2진수 9자리로 만들고 계산하기

var test1 = "100000000"
var test2 = UInt(test1, radix: 2)
var test3 = "1111111101"
var test4 = UInt(test3, radix: 2)

let result = test2! + test4!
let test5 = String(result, radix: 2)

print(test5) // "10011111101"

2진수는 8자리입니다. 그런데 계산기에는 9자리까지 받더라구요...

그래서 고민 끝에 만들어 봤습니다..한 5시간 걸린것 같아요😂 🤣

로직은 이렇게 짜봤습니다.

  • 먼저 String 값을 가지고 있는 test1, test3를 Uint타입 2진수로 변환 시켜줬습니다.
  • 이렇게 변환 된 Uint 타입으로 필요한 계산을 진행 하였습니다.
  • 계산이 끝난 뒤 다시 test5 처럼 String으로 결과값을 반환하도록 했습니다.

이 방식이 best는 아닌 것 같지만 그래도 해결했다는 것에 의미를~~~

profile
james, the enthusiastic developer

0개의 댓글