밤에 쓴 코드

Clean Code) 단위테스트 본문

Clean Code

Clean Code) 단위테스트

붱이🦉 2019. 7. 10. 18:52

단위테스트

  • 개념
    • 테스트가 가능한 (최소)단위-모듈로 나누어진 소프트웨어 내에서 결함을 찾고 기능을 검증하는 활동

TDD (테스트 주도 개발)

  • 실제 코드를 직성하기 전에 실패하는 테스트 코드를 작성한다.
  • 실패는 하지만 컴파일은 성공할 수 있게끔 작성한다.
  • 테스트를 통과할 정도의 간단한 코드로 실제 코드를 작성한다

위의 3가지 원칙을 차례대로 지키면서 개발을 진행하며, 사이클을 계속적으로 수행한다.

그러면 테스트는 어떻게 작성해야 할까?

'테스트 코드는 깨끗하게 작성하라' 라고 책에서 강조를 한다.

더러운 테스트 코드는 어떤 피해를 주는 지 한번 보자.

테스트 코드도 코드이다. 실제 코드가 변하게 되면, 테스트코드도 같이 변해야 한다.

테스트 코드가 더럽다면 또는 복잡하다면, 늘어가는 테스트 코드는 개발자에게 큰 부담이 된다.

매 구현마다 그 더러운 코드를 보며 부담을 느낄 것이다.

결국은 테스트를 포기할 지도 모르겠다.

테스트 슈트가 없는 프로젝트에서의 모든 변화는 잠정적인 버그의 가능성이다.

버그가 숨어들 틈이 너무나도 많아진다. 매 변화마다 늘어가는 버그에 개발자는 지쳐갈 것이다.

변화를 무서워하여 변화를 회피하는 방향이 될 수도 있다.

반면 테스트 슈트가 존재한다면 , 변화가 두렵지 않다. 변화에 따른 버그를 쉽게 매번 검증해 낼 수 있기 때문이다.

테스트 커버리지: 테스트의 대상에 대해서 현재진행 된 범위(정도)가 커진다면 아마 더 변화가 무섭지 않을 것이다.

이 처럼 테스트 코드는 실제 개발에서 이로운 점이 많이 존재한다.

간단한 예시를 적어보겠다.

func power(_ root: Int, _ index: Int) -> Int {
          return 0
}

나는 power() 메소드를 구현할 예정이다. 이제 구현에 앞서, 테스트 메소드를 작성할 것이다.

func testPower() {
  //Given
  let num1 = 2                //     입력1
  let num2 = 10                //    입력2
  let expected = 1024      //     기대값 

  //When
  let result = multifly(num1, num2)    // 메소드의 결과

  //Then
  XCTAssertEqual(result,expected)    //비교
}

❗️검증메소드는 사실 좀 더 많은 것이 좋을 것 같지만 생략하겠다.

이렇게 테스트 메소드를 작성했다.

  • Given - 테스트에 필요한 데이터를 생성한다.
  • When - 테스트하고 싶은 특정 상황에서 데이터를 조작한다.
  • Then - 테스트 대상 메소드의 출력과 생각했던 기대값으로 성공/실패를 검증한다.

우리가 테스트하는 목적을 생각해보자.

이 후 있을 변화에

power() 메소드는 구현을 하지않았다. 당연히 ❌ 실패한다. 컴파일은 오류를 뱉지 않는다.

이제 구현할 시간이다.

이제 구현을 해보자.

func power(_ root: Int, _ index: Int) -> Int {
       let result = 1
       for _ in 0..<index {
        result = result * 1
     }
          return result
}

테스트는 성공적으로 ✅ 동작하는 걸 볼 수 있다.

그런데 power() 메소드가 동작은 제대로 하지만 맘에 들지 않는다. 더 보기 좋고 나은 코드로 개선해보고 싶은 데

잘 하던 동작이 엉뚱하게 동작할까봐 겁이 날 것이다.

예시처럼 단순한 메소드는 모르겠지만, 실제 프로젝트에서 하는 메소드들은 변화에 대한 부담감이 실로 엄청 날 것이다.

테스트 코드는 그 두려움을 덜어 줄 수 있다.

우리는 테스트를 잘 작성해뒀기 때문에 더 편안한 마음으로 도전할 수 있다.

func power(_ root: Int, _ index: Int) -> Int {
   return Array.init(repeating: root, count: index).reduce(1){ $0*$1 }
}

깔꼼하게 수정해보았다!

내 마음에는 쏙 든다. 하지만 제대로 동작하는 지 검증하기 전까지는 불안함을 떨칠 수가 없다.

무사히 제대로 동작하는 것을 테스트로 검증하면 이제 끝이다.✅

아마 테스트 코드가 사전에 작성되어 있지 않았다면, 매번 하는 재수정이 번거롭고, 검증작업또한 자동화되지 않아서 시간이 오래 걸렸을 것이다.

이제 테스트에 대해 장점은 어느정도 느낌이 올 것이다.

이제 그럼 테스트 코드를 작성할 때 어떤 고려할 사항이 있을까?

한 테스트메소드는 하나의 개념을 테스트해라

예시 코드를 작성 해 보겠다.

func testKFC() {
          // Given
      var kfc = KFC()

          // When
          kfc.buy(menu: .chicken, quantity: 1)
          kfc.trim()
          kfc.fry()
        let chicken = kfc.serve()

          // Then
          XCTAssertTrue(chicken.flvor,.fried)
}
func testKFC2() {
          // Given
      var kfc = KFC()

          // When
          kfc.buy(menu: .chicken, quantity: 1)
          kfc.trim()
          kfc.fry()
        _ = kfc.serve()
          let remain = kfc.remain

          // Then
          XCTAssertTrue(remain, 0)
}

위의 테스트를 보면 중복되는 //When 구간이 거의 중복되는 걸 보니, 불편한 마음을 떨칠 수가 없다.

func testKFC() {
          // Given
      var kfc = KFC()

          // When
          kfc.buy(menu: .chicken, quantity: 1)
          kfc.trim()
          kfc.fry()
        let chicken = kfc.serve()
          let remain = kfc.remain

          // Then
          XCTAssertTrue(chicken.flvor,.fried)
          XCTAssertTrue(remain, 0)
}

분명히 중복되는 코드는 없어졌지만 테스트 메소드가 테스트하는 대상이 불명확해진 것 같다는 느낌이 들었다.

이런 식의 테스트는 템플릿메소드패턴 이나 swiftsetUp()메소드로 어느정도 자동화 해줄 수 있다.

하지만 위의 두개를 동시에 하는 테스트는 적절하지 않다고 나는 느끼고 차라리 중복되더라도

위의 예시처럼 분리해두는 것이 더 좋은 테스트라고 개인적으로 생각한다.

마지막으로

테스트 5원칙 F.I.R.S.T

Fast

Independent

Repeatable

Self-validating

Timely


  • 테스트는 빨라야한다.

  •  왜냐면 테스트는 자주자주자주자주 해야 하기 때문이다.
  • 테스트는 다른 테스트들과 독립적이어야 한다.

    테스트가 순서가 있다고 생각해보자.
    이 A-테스트를 성공한 다음 B-테스트 그 다음 C-테스트
    중간에 하나의 테스트만 문제가 생겨도 의존된 테스트의 결과에도 영향을 준다면
    제대로 동작할 수 있는 테스트가 실패하는 경험도 할 수 있을 것이다.
  • 테스트는 반복가능해야한다.

  • 이전에 나는 테스트 I,R을 동시에 시원하게 어겨 본 적이 있었다.
    
    npm 의 superTest,Mocha.js 를 이용한 테스트였는 데,
    'User-1'로 회원가입 요청을 하는 테스트 메소드, 'User-1'로 로그인 요청을 하는 테스트 메소드를 만든 적이 있다.
    위의 I조건 처럼 회원가입요청이 실패하면 덩달아 실패하는 테스트가 다른 테스트에 의존하는 구조였으며,
    회원가입요청을 성공하면 실제 DB에 정보가 저장되어서, 그 다음테스트를 위해서는 DB를 깨끗하게 정리해야 했다.
    결국 나는 어느 날부터 테스트 코드를 방치했다.
    
    나의 경험으로 빗대어 봐도, 조건들이 테스트에 어떤 영향을 미치는 지 느낄 수 있다고 생각한다.
  • 테스트는 자가검증이 가능 해야한다

  • 객체가 동작을 수행하면, 이제 동작이 내가 기대한 방향으로 움직였는 지를 검증하려고 
    테스트를 작성했을 것이다. 
    
    회원가입요청에서 만약 회원가입이 성공했는 지, 실패했는 지를 반환하는 요청에 대한 응답이 없다고 생각해보자.
    아마 요청을 수행하고, DB에 가서 실제로 가입이 성공했는 지 확인해야한다면 얼마나 테스트가 피곤한 작업이 될까?
  • 테스트는 시기적절하게 작성해야한다

  • 테스트 코드를 작성하면서 얻는 건, 이 후에 검증의 자동화 뿐이 아니다.
    테스트 코드를 작성하게되면, 
    내가 앞으로 설계해야할 메소드, 객체에 어떤 설계를 해야할 지, 어떤 문제가 발생할 수 있는 지를 미리 알 수 있다.

테스트를 열심히 하는 개발자가 되어 보자.

'Clean Code' 카테고리의 다른 글

Clean Code ) 클래스  (0) 2019.07.29
Clean Code ) 객체와 자료구조  (0) 2019.06.23
Clean Code) 형식 맞추기  (0) 2019.06.08
Clean Code) 주석  (0) 2019.06.08
Clean Code) 함수  (0) 2019.05.02
Comments