클린코드 (7) - 경계와 테스트

gentledot·2021년 6월 20일
0

경계

  • 시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다.
    • 외부 패키지
    • 오픈소스
    • 다른 팀이 제공하는 컴포넌트
  • 어떤 식으로든 외부 코드를 우리 코드에 깔끔하게 통합해야만 한다.

외부 코드 사용하기

  • 인터페이스 제공자 ↔ 인터페이스 사용자

    • 패키지 제공자 또는 프레임워크 제공자 : 적용성을 최대한 넓히려함. (더 많은 환경에서 돌아가야 하기 때문)
    • 사용자 : 자신의 요구에 집중하는 인터페이스를 바람
  • 다양한 인터페이스로 수많은 기능을 제공하지만 그만큼 위험도 크다.

    • ex) java.util.Map (Interface Map - JDK 11 Api Docs.)
      Map의 methods
      Map의 methods(2)

    • 넘기는 쪽에서는 아무도 Map 내용을 삭제하지 않을지도 모르지만 Map 사용자라면 누구나 Map 내용을 지울 권한이 있다. (Map.clear())

    • Map에 특정 객체 유형만 저장하기로 결정했다고 하지만 Map은 객체 유형을 제한하지 않기 때문에 마음만 먹으면 사용자는 어떤 객체 유형도 추가할 수 있음

      • ex) 맵의 생성과 객체 조회

        Map sensors = new HashMap();
        Sensor s = (Sensor) sensors.get("sensorId");
      • Map이 반환하는 Object를 올바른 유형으로 변환할 책임은 Map을 사용하는 client에 있다.

      • 코드는 동작하지만 깨끗한 코드라 보긴 어려움. 또한 의도도 분명히 드러나지 않음.

      • Generics를 사용하여 Map에 담긴 내용에 대한 가독성을 높힐 수 있음. (Map<String, Sensor>)하지만 사용자가 필요하지 않은 기능까지 제공한다는 문제는 해결하지 못한다.

      • 프로그램에서 Map 인스턴스를 여기저기로 넘긴다면 Map 인터페이스가 변할 경우 수정할 코드가 상당히 많아진다.

    • Map을 Sensors 클래스 안으로 숨겨서 좀 더 깔끔하게 사용할 수 있음

      public class Sensors {
          private Map sensors = new HashMap();
          public Sensor getById(String id) {
              return (Sensor) sensors.get(id);
          }
      
          		...
      
      }
      • 경계 인터페이스인 Map을 Sensors 안으로 숨겨 class 안에서 객체 유형을 관리하고 변환한다. ⇒ Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.
      • Sensors class는 프로그램에 필요한 인터페이스만 제공한다. 코드는 이해하기 쉽지만 오용하기는 어려워짐. ⇒ class를 통해 (나머지 프로그램이) 설계 규칙과 비즈니스 규칙을 따르도록 강제할 수 있음.
      • Map class를 사용할 때마다 캡슐화를 해야한다는 의미는 아님. Map을 (혹은 유사한 경계 인터페이스를) 여기저기 넘기지 말라는 의미!
      • 경계 인터페이스 (Map 포함)를 이용할 때는 이를 이용하는 class나 class 계열 밖으로 노출되지 않도록 주의한다. Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.

자신이 관리하고, 입력하고 구현하는 객체, 클래스, 메서드 바깥으로 결과값을 내보낼 때는 별 신경쓰지 않고 내보낼 수 있는 객체면 좋겠지만... 웬만하면 다른 사용자가 쓰기 편한 객체르 내보내는 것이 좋은 것 같습니다.

Map<String, Object> 와 같은 return 값을 대할 때 드는 생각들을 주절주절해보자면

  • Map의 유효한 key는 무엇일까? -> put()을 찾아봐야겠다. ㅠ
  • 발견한 key로 조회할 수 있는 데이터타입은 무엇일까? 어떻게 cast 해야 할까?
  • 조회한 데이터가 객체 클래스라면 어떤 역할(method)를 요청할 수 있을까?
  • 조회한 데이터가 null 일 가능성이 있을까?

경계 살피고 익히기

  • 외부 코드를 사용하면 적은 시간에 더 많은 기능을 출시하기 쉬워진다.
  • 외부 패키지 테스트에 대한 책임은 없지만 사용할 코드에 대해 테스트를 하는 편이 바람직
  • 외부 코드를 익히기 어렵고, 통합하기도 어렵다. 두 가지를 동시에 하기에는 두 배나 어렵다.
  • 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 방법이 있음
    • Jim Newkirk는 이를 학습 테스트 (테스트 주도 개발 p222-237) 라 부른다.
    • 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈.
    • 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.

log4j 익히기

Logback, Log4j2, Log4jdbc-log4j2, log4sql 을 경험해봤다고는 이야기 할 수 있겠지만 잘 안다라고는 자신있게 말하긴 어려울 것 같습니다. 추후에 내용을 정리해보고 싶습니다.

  • 로깅 기능을 직접 구현하는 대신 apache의 log4j 패키지를 사용하려 한다고 가정

테스트 케이스 작성

  • 화면에 "hello"를 출력하는 테스트 케이스

    @Test
    public void testLogCreate() {
    		Logger logger = Logger.getLogger("MyLogger");
    		logger.info("hello");
    }
  • 테스트 케이스가 돌아가지 않음 : Appender라는 뭔가가 필요하다는 오류 발생

    • ConsoleAppender class 필요
  • ConsoleAppender 생성한 테스트

    @Test
    public void testLogCreate() {
    		Logger logger = Logger.getLogger("MyLogger");
    		ConsoleAppender appender = new ConsoleAppender();
    		logger.addAppender(appender);
    		logger.info("hello");
    }
    • 테스트 케이스가 돌아가지 않음 : Appender에 출력 스트림이 없음
  • 출력 스트림 추가한 테스트

    @Test
    public void testLogCreate() {
    		Logger logger = Logger.getLogger("MyLogger");
    		logger.removeAllAppenders();
    		logger.addAppender(new ConsoleAppender(
    				new PatternLayout("%p %t %m%n"),
    				ConsoleAppender.SYSTEM_OUT));
    		logger.info("hello");
    }
    • 로그 메시지가 콘솔에 찍힘
    • ConsoleAppender.SYSTEM_OUT를 제거해도 "hello"가 찍힘 ⇒ 기본 ConsoleAppender 생성자는 '설정되지 않은' 상태이기 때문
  • log4j로 단위 테스트 케이스 표현

    public class LogTest {
    		private Logger logger;
    		
    		@Before
    		public void initialize() {
    				logger = Logger.getLogger("logger");
    				logger.removeAllAppenders();
    				Logger.getRootLogger().removeAllAppenders();
    		}
    
    		@Test
    		public void basicLogger() {
    				BasicConfigurator.configure();
    				logger.info("basicLogger");
    		}
    
    		@Test
    		public void addAppenderWithStream() {
    				logger.addAppender(new ConsoleAppender(
    						new PatternLayout("%p %t %m%n"),
    						ConsoleAppender.SYSTEM_OUT));
    				logger.info("addAppenderWithStream");
    		}
    
    		@Test
    		public void addAppenderWithoutStream() {
    				logger.addAppender(new ConsoleAppender(
    						new PatternLayout("%p %t %m%n")));
    				logger.info("addAppenderWithoutStream");
    		}
    		

학습 테스트는 공짜 이상이다

  • 어쨌든 API를 배워야 하기 때문에 테스트에 드는 비용은 불가피. (테스트로 배우거나 또는 직접 사용하며 배우거나)
    • 학습이 필요하든, 필요하지 않든 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다.
  • 학습 테스트는 이해도를 높여주는 정확한 실험이다.
  • 패키지 새 버전이 나오면 학습 테스트를 돌려 차이가 있는지 확인한다.
    • 통합 후라도 패키지가 우리 코드와 호환되리라는 보장은 없다.
    • 패키지 작성자는 버그를 수정하고 기능도 추가한다.
    • 새 버전이 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.
  • 학습 테스트를 통한 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.
    • 그렇지 않다면 낡은 버전을 필요 이상으로 오랫동안 사용하려는 유혹에 빠지기 쉽다.

낡은 버전을 오랫동안 사용하려는 유혹은 정말로 뿌리치기 힘든 것 같습니다. 변화 보다는 안정을 추구하는 조직이더라도 변경에는 대비해야 나중에 큰 문제에 당면하지 않을 것이라 생각합니다.

아직 존재하지 않는 코드를 사용하기

  • 경계와 관련한 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계이다.

  • 시스템과 다른 시스템간의 경계 또는 지식이 경계를 너머 미치지 못하는 코드 영역도 있음

  • ex) 송신기 모듈에 원하는 기능 =>
    지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라

    그림 8.2 송신기 예측하기

    • 자체적인 인터페이스 정의 : Transmitter class를 만든 후 transmit라는 메서드를 추가
      • Transmit interface는 주파수와 자료 스트림을 입력으로 받음
      • 바라는 인터페이스를 구현하면 외부 인터페이스를 전적으로 통제한다는 장점이 생김 + 코드 가독성 상승, 코드 의도도 분명해지는 효과
    • 통제하지 못하고 정의되지 않은 외부 API인 송신기 API에서 CommunicationsController를 분리
    • API가 정의된 뒤에는 TransmitterAdapter를 구현해 간극을 메움 (Adapter Pattern)
    • Adapter Pattern으로 API 사용을 캡슐화하여 API가 바뀔 때 수정할 코드를 한 곳으로 모음
      • 테스트를 편하게 작성 가능
        • 적절한 FakeTransmitter class를 사용하면 CommunicationsController class를 테스트 할 수 있음
        • Transmitter API interface가 나온 다음 경계 테스트 케이스를 생성해 API를 올바로 사용하는지 테스트할 수도 있다.

디자인 패턴에 대해서는 좀 더 공부가 필요하다고 느낍니다. 그럼에도 최대한 정리해보자면

  • interface를 통하면 역할을 수행하는 class를 갈아끼우기 편하다.
  • Adapter 패턴을 통해 역할을 외부에서 의존하여 구성(field 값으로 설정) 하면 요구되는 타입이나 객체가 다르더라도 원하는 역할을 수행시킬 수 있다.
  • 요구되는 역할을 정의해두었기 때문에 Mock 객체로 생성하기 수월할 것 같다.

깨끗한 경계

  • 변경 시 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. (엄청난 시간, 노력, 재작업을 요구하지 않음)
    • 통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.
  • 경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.
    • 외부 패키지를 세세히 알 필요 없음. 외부 패키지에 의존하는 대신 통제 가능한 우리 코드에 의존하는 편이 훨씬 나음. (외부 코드에 휘둘리지 않도록)
  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
    • 새로운 class로 경계를 감싸거나
    • Adapter Pattern을 사용해 원하는 interface를 패키지가 제공하는 interface로 변환
  • 경계를 관리하면 코드 가독성이 높아지고, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다

외부 코드에 휘둘리지 않는다는 것을 외부 의존성에 대해서는 의도한 대로 동작하는 것을 확인하는 정도만 신경써도 괜찮다는 의미로 이해했습니다. 내부 동작이 어떤지는 블랙박스마냥 여기고, 반환하는 데이터를 어떻게 가공할 것인지에 대해 로직 구현에 집중하라는 의미로 와닿았습니다.

원하는 interface를 생성해두는 방식은 차후에 작성할 기회가 생기면 도전해보고 싶습니다. 필요한 기능을 찾아 API를 뒤적이는 것이 아니라 필요한, 그리고 기대한 기능만 가져다 쓰는 관점이 흥미롭게 생각되었습니다.

단위테스트

  • 단위테스트란?
    • 자기 프로그램이 '돌아간다'는 사실만 확인하는 일회성 코드?
    • agile, TDD → 단위 테스트의 자동화
  • 테스트 추가 이전에 제대로 된 테스트 케이스를 작성해야 한다는 좀 더 미묘한(더욱 중요한) 사실을 놓친건 아닐까?
  • 단위테스트 + 깨끗한 테스트 코드
    • 유연성, 유지보수성, 재사용성을 보존하고 강화할 수 있음
    • 지속적으로 표현력을 높이고 간결하게 정리하여 깨끗한 코드가 되도록 관리하자
    • 테스트 API를 구현해 Domain Specific Language(DSL)를 만들면 테스트 코드를 짜기가 수월해진다.
  • 테스트 코드가 방치되어 망가지면 실제 코드도 망가진다. ⇒ 테스트 코드도 깨끗하게 유지하자.

DSL이라 유식한 척 말을 꺼낼 순 있겠지만 무엇인지 설명하라하면 잘 설명할 수 없는 관계로... 나중에 공부해둘 키워드로 선정!

TDD 법칙 세 가지

  • TDD의 3법칙
    • 첫째 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
    • 둘째 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
    • 셋째 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
  • 세 가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 묶인다.
    • 테스트 코드와 실제 코드가 함께 나올 수 있음
    • 테스트 코드가 실제 코드보다 불과 몇 초 전에 나옴
  • 실제코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발한다.

방대한 코드는 관리하기 정말 어렵습니다. 특히 구현된 legacy code에 테스트를 작성하려고 시도해보면 단위로 빼는게 정말 어렵기 때문입니다.

깨끗한 테스트 코드 유지하기

  • 테스트를 안하느니 차라리 지저분한 테스트 코드라도 있는 편이 좋지 않을까?
    • 테스트 코드는 일회성으로 작성되는 코드가 아니다.
    • 실제 코드가 진화하면 테스트 코드도 변해야 하는데 코드가 지저분할수록 변경하기 어려워진다.
      • 테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸리기 십상...
  • 새 버전 출시마다 팀이 테스트 케이스를 유지하고 보수하는 비용도 증가한다. → test suite 폐기를 고려하는 상황까지...
  • test suite가 없으면 개발자 자신이 수정한 코드가 제대로 도는지 확인할 방법이 없음
    • 의도하지 않은 결함을 검증할 방법이 없음 → 결함율 증가 → 변경을 주저하게 됨 → 변경을 주저하면서 더 이상 코드를 정리하지 않음 → 코드가 망가지기 시작

⇒ 테스트 코드는 실제 코드 못지 않게 중요하다. (사고와 설계와 주의가 필요하다.)

"변경을 주저하게 됨" <- 코드를 작성하는데 이 생각이 든다면 코드 변경을 쉽게 시도할 수 없게 되는 것 같습니다. 제 자신 뿐 아니라 다른 팀원에게도 권하기 어렵기도 하구요.

테스트는 유연성, 유지보수성, 재사용성을 제공

  • 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위테스트!
    • 테스트 케이스가 없다면 모든 변경이 잠정적인 버그가 된다.
    • 아키텍처가 아무리 유연하더라도, 설계를 아무리 잘 나눴더라도, 테스트 케이스가 없으면 개발자는 변경을 주저한다... 버그가 숨어들까 두렵기 때문에.
    • 테스트 커버리지가 높을수록 공포는 줄어든다.
  • 실제 코드를 점검하는 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠
  • ↔ 테스트 코드가 지저분하면
    • 코드를 변경하는 능력이 떨어짐
    • 코드 구조를 개선하는 능력도 떨어짐
    • 실제 코드도 지저분해짐

커버리지가 높을수록 정말로 공포가 줄어드는지는 잘 모르겠지만 커버리지가 높다고 안정적인 코드를 뜻하는 것은 아니라고 생각합니다.

깨끗한 테스트 코드

  • 깨끗한 테스트 코드 = 높은 가독성!

  • 가독성을 높이려면

    • 명료성
    • 단순성
    • 풍부한 표현력
  • 개선이 필요한 테스트 코드의 예 - FitNess.SerializedPageResponderTest.java

    FitNess.SerializedPageResponderTest(1)

    FitNess.SerializedPageResponderTest(2)

    FitNess.SerializedPageResponderTest(3)

    • PathParser는 문자열을 pagePath instance로 변환한다. pagePath는 웹 로봇(crawler)이 사용하는 객체이므로 테스트와 무관하고 테스트 코드의 의도만 흐린다.
    • responder 객체를 생성하는 코드와 response를 수집해 변환하는 코드 역시 잡음에 불과함.
    • resource와 인수에서 요청 URL을 만드는 어설픈 코드도 보인다.(고 한다.)
    • 해당 테스트 코드는 읽는 사람을 고려하지 않는다. (온갖 잡다하고 무관한 코드를 이해한 뒤 간신히 테스트 케이스를 이해할 것임...)
  • 위 테스트와 동일한 테스트를 수행하는 리팩토링된 테스트

    FitNess.SerializedPageResponderTest_refactoried(1)

    FitNess.SerializedPageResponderTest_refactoried(2)

    • Build - Operate - Check 패턴의 테스트 구조 (Arrange - Act - Assert)
      • 첫 부분은 테스트 자료를 생성
      • 두 번째 부분은 테스트 자료를 조작
      • 세 번째 부분은 조작한 결과가 올바른지 확인
    • 잡다하고 세세한 코드를 거의 다 제거한 코드
      • 진짜 필요한 자료 유형과 함수만 사용
      • 코드를 읽는 사람은 코드가 수행하는 기능을 재빨리 이해하게 됨.

도메인에 특화된 테스트 언어

  • 위의 리팩토링 된 테스트 코드는 도메인에 특화된 언어 (DSL)로 테스트 코드를 구현하는 기법을 보여준다.
    • 흔히 쓰는 시스템 조작 API를 사용하는 대신 API 위에 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용 → 테스트 코드를 짜기도 읽기도 쉬워진다.
  • 처음부터 설계된 API는 아님. 자기 코드를 좀 더 간결하고 표현력이 풍부한 코드로 리팩토링 하면서 다듬어진 결과물

이중 표준

  • 테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다.

    • 단순하고, 간결하고, 표현력이 풍부해야 하지만
    • 실제 코드만큼 효율적일 필요는 없다.
    • 실제 환경이 아닌 테스트 환경에서 돌아가는 코드이기 때문.
  • 이중 표준이 쓰인 테스트의 예 - 환경 제어 시스템 테스트

    @Test
    public void turnOnLoTempAlarmAtThreashold() throws Exception {
    	 hw.setTemp(WAY_T00_C0LD);
    	 controller.tic();
    	 assertTrue(hw.heaterState());
    	 assertTrue(hw.blowerState());
    	 assertFalse(hw.coolerState());
    	 assertFalse(hw.hiTempAlarm());
    	 assertTrue(hw.loTempAlarm());
    }
    • 세세한 사항이 많은 테스트..
      • tic 함수가 무엇일까?
    • 시스템의 최종 상태가 온도가 '급강하' 했는지 확인하는 것이 주요
    • 코드를 점검하는 상태 이름과 상태 값을 확인하느라 눈길이 분산된다. (heaterState 확인 → assertTrue 확인)
  • 코드 가독성을 높인 리팩토링

    @Test
    public void turnOnLoTempAlarmAtThreashold() throws Exception {
    		wayTooCold();
    		assertEquals("HBchL", hw.getState());
    }
    • tic 함수는 wayTooCold 함수를 만들어 숨김.
    • 상태 표현을 heater, blower, cooler, hi-temp-alarm, lo-temp-alarm 순서로 대문자는 켜짐, 소문자는 꺼짐을 표현함
    • 위 방식은 그릇된 정보를 피하라는 규칙의 위반에 가까우나 의미만 안다면 눈길이 문자열을 따라 움직이며 결과를 재빨리 판단한다.
  • 가독성을 높인 테스트 코드는 이해하기 쉽다

    EnvironmentControllerTest - 더 복잡한 선택

  • 상태 표현을 반환하는 getState()

    public String getState() {
    		String state = "";
    		state += heater ? "H" : "h";
    		state += blower ? "B" : "b";
    		state += cooler ? "C" : "c";
    		state += hiTempAlarm ? "H" : "h";
    		state += loTempAlarm ? "L" : "l";
    		return state;		
    }
    • String의 결합에 효율을 높이려면 StringBuffer()를 사용해야 한다
    • 하지만 테스트 환경이니까 자원의 제한적일 가능성이 낮으므로 좀 더 보기 편한 방식을 써도 무관 (StringBuffer를 쓰지 않아 치르는 대가가 미미함)
    • 이중 표준 = 실제 환경에서는 절대로 안되지만 테스트 환경에서는 메모리나 CPU효율과 관련 있는 경우를 고려하지 않아도 되기 때문에 이중 표준을 가질 수 있음

최초에는 실패하는 테스트를 작성하며 부담없이 로직을 정리해가는 것이니까 성능을 고려하기 보다는 로직을 알아보기 편한 코드를 우선 고려하는게 더 나은 것 같습니다.

테스트 당 assert 하나

  • jUnit으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용해야 한다 주장하는 학파가 있음

    One Assertion Per Test

    • 가혹한 규칙이라 할 수 있지만 assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.
  • 테스트를 쪼개 각자가 assert를 수행할 수 있도록 구성할 수 있음

    SerializedPageResponderTest - 단일 assert

    • 함수 이름이 given-when-then 관례에 따라 명칭을 변경하였음
    • 테스트를 분리하면 중복되는 코드가 많아진다.
    • Template method 패턴을 사용하면 중복을 제거할 수 있음
      • given/when 부분을 부모 클래스에 두고 then부분을 자식 클래스에 두는 방식
      • 또는 독자적인 테스트 클래스를 만들어 @Before 함수에 given/when 부분을 넣고 @Test 함수에 then 부분을 넣기
  • 단일 assert 문의 규칙은 훌륭한 지침이지만 효율적이고 관리가 편한 코드 측면에서는 여러 assert 문을 넣어도 무관함. (assert 문 개수는 최대한 줄여야 좋다.)

테스트 당 개념 하나

  • 이것저것 잡다한 개념을 연속으로 테스트 하는 긴 함수는 피한다.

  • 여러 개념을 한 함수로 몰아넣으면 독자가 각 절이 존재하는 이유와 각 절이 테스트 하는 개념을 모두 이해해야 하기 때문

  • 여러 개념이 모여있는 테스트 코드의 예

    addMonths 메서드를 테스트하는 장황한 코드

  • 테스트를 셋으로 분리한다면?

    • 31일로 끝나는 달의 마지막 날짜가 주어지는 경우
      1. (6월처럼) 30일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되어서는 안 된다.
      2. 두 달을 더하면 그리고 두 번째 달이 31일로 끝나면 날짜는 31일이 되어야 한다.
    • 30일로 끝나는 달의 마지막 날짜가 주어지는 경우
      1. 31일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되면 안된다.
  • 장황한 테스트 코드 속 일반적인 규칙이 드러난다.

    • 날짜에 어떤 달을 더하면 그 달의 마지막 날짜보다 커지지 못한다. (2월 28일 + 1달 = 3월 28일) ⇒ 누락된 테스트이므로 보완 필요-
  • 가장 좋은 테스트 규칙 ⇒ 한 테스트에는 개념 당 assert 문 수를 최소로 줄이고, 테스트 함수 하나는 개념 하나만 테스트 할 것!

하나의 테스트에는 한 두개의 개념을 확인하는 assert를 작성하고 (알고 싶은 단언은 전부 작성해버리고) 개념은 하나만 테스트, 부족한건 다른 테스트에서 확인하는 방식으로 정리할 수 있을 것 같습니다.

F.I.R.S.T

  • 깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.
  • Fast, Independent, Repeatable, Self-Validating, Timely

First (빠르게)

  • 테스트는 빨라야 한다.
  • 빠르면 자주 돌릴 수 있다.
  • 자주 돌리지 못하면 초반에 문제를 찾아내 고치지지 못한다.
  • 코드를 마음껏 정리하지 못해 코드 품질이 망가지기 시작한다.

Independent (독립적으로)

  • 각 테스트는 서로 의존하면 안 된다.
  • 한 테스트가 실행될 환경을 준비해서는 안됨.
  • 각 테스트는 독립적으로, 어떤 순서로 실행되도 괜찮아야 한다.
  • 테스트가 서로에 의존하게 된다면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워짐. (후반 테스트가 찾아내야 할 결함이 숨겨진다.)

독립적으로 수행하지 못하는 테스트를 작성할 때는 @Ignore를 설정해 사유를 적어두긴 했지만 어떻게 해야 하는지 고민입니다.

Repeatable (반복가능하게)

  • 테스트는 어떤 환경에서도 반복 가능해야 한다.
  • 실제 환경, QA환경, 네트워크에 연결되지 않은 환경 등 어디서든 테스트가 실행할 수 있어야 한다.
  • 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다.
  • 환경이 지원되지 않아 테스트를 수행하지 못하는 상황에 직면한다.

Self-Validating (자가검증하는)

  • 테스트는 bool (boolean) 값으로 결과를 내야 한다. (성공 또는 실패)
  • 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안된다.
  • 통과 여부를 보려고 텍스트 파일 두 개를 수작업으로 비교하게 만들어서도 안 된다.
  • 테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적이 되고, 지루한 수작업 평가가 필요하게 된다.

Timely (적시에)

  • 테스트는 적시에 작성해야 한다.
  • 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
  • 실제 코드를 구현한 다음 테스트 코드를 만들면 실제 코드를 테스트 하기 어렵단 사실을 발견할 지 모름
  • 어떤 코드는 테스트하기 어렵다고 판명날지도 모르고..
  • 테스트가 불가능하도록 실제 코드를 설계할지도 모름
profile
그동안 마신 커피와 개발 지식, 경험을 기록하는 공간

0개의 댓글