Today I../Today I Read

[Clean code] Chapter 09. Unit test

HJChung 2022. 2. 19. 16:13
Clean Code 클린 코드 - 로버트 C. 마틴 저
를 읽고, clean code 해설 강의를 통해 제가 이해한 바를 정리한 글입니다. 

 

 

테스트 코드를 추가하는 것을 넘어서 제대로 된 테스트 케이스를 작성해야 한다. 

 

 

1. 테스트 코드의 중요성

  • 테스트 코드는 실수를 바로 잡아준다.
  • 코드에 유연성, 유지보수성,  재사용성을 제공하는 버팀목이 단위 테스트이다. 테스트 케이스가 없다면 모든 변경이 잠정적인 버그인 반면 테스트 케이스가 있으면 코드 변경이 두렵지 않다. 

그래서 테스트 커버리지가 높을 수록 버그에 대한 공포가 줄어든다. 

 

강의에서 추천 해 준 책 'Effective Unit Testing - 라쎄 코스켈라' 에서 말하는 테스트의 중요성에는

  • 테스트는 실사용에 적합한 설계를 끌어내준다. 
  • 테스트를 작성해서 얻게 되는 가장 큰 수확은 테스트 자체가 아니다. 작성 과정에서 얻는 깨달음이다. 

라고 소개한다. 

즉, 테스트는 실수를 바로 잡아주고, 코드 변경의 버팀목이 되어주기도 하지만 각각의 로직을 테스트해보면서 실사용에 적합한 설계란 무엇인지에 대해 다시 생각해보게 해주기도 하는 측면에서도 역시 중요하다. 

 

2. 테스트 자동화

이렇게 중요한 테스트의 실행은 자동화시키는게 좋다. 

테스트 실행을 양심에 맡기는 것 보다 시스템에게 맡기는게 여러모로 효율적이라고 생각한다. 마치 우리 팀에 코드리뷰 잔소리봇(Slack alert)이 생기고 나서부터는 번거롭게 메일 확인을 하지 않고도 코드리뷰 요청이 있다는 것을 적시에 알람받을 수 있게 되면서 팀 생산성이 높아진 것 처럼 말이다. 

 

CI(지속적 통합)가 도입되면 개발자들이 main 브랜치에 새로운 코드나 코드 변경사항을 업로드할 때마다 자동으로 빌드 및 전체 유닛 테스트를 실행하여서 애플리케이션에 제대로 적용되었는지 검사를 하게 된다. 그리고 테스트가 실패한 경우에는 알람을 보내 해당 내용을 업로드한 개발자가 문제를 해결하게 한다.

 

 

3. 테스트의 종류

  • Unit Test: 프로그램 내부의 개별 컴포넌트의 동작을 테스트한다. 
  • Integration Test: 프로그램 내부의 개별 컴포넌트들을 합쳐서 동작을 테스트한다. Unit Test에서는 각 컴포넌트를 독립적으로 테스트하기 때문에 컴포넌트의 interaction을 확인하는 Integration Test가 필요하다. 
  • E2E Test(End to End Test): 실제 유저의 시나리오대로 네트워크를 통해 서버의 Endpoint를 호출해 테스트하여 개발자가 기대한대로 이 서비스가 동작하는지 유저 관점에서 테스트한다. 

구글에서는 Unit test 70%, Integration test 20%, E2E Test 10%의 비율로 작성할 것을 제안한다. 

 

4. 깨끗한 테스트 코드

1) 테스트 라이브러리를 사용하자. 

  • Jest: for JavaScript testing.  It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!
  • Cypress: JavaScript End to End Testing Framework
  • Unittest, Pytest: for Python Testing. 

내 경우 현업에서 Jest와 Unittest를 많이 사용한다. 

강의에서 알려준 Java 기준으로는 JUnit + Mockito를 많이 사용한다고 한다. 

    • JUnit: for unit test
    • Mockito: for mocking dependencies
    • wiremock: for stubbing out external services
    • Pact: for writing CDC tests
    • Selenium: for writing UI-driven End to End Testing
    • REST-assured: for writing REST API-driven End to End Testing

 

2) 적절한 Test Double을 선택해서 구현하자. 

Test Double은 테스트에서 원본 객체을 대신하는 객체를 말한다. 스턴트맨이라고 알고 있는, 실제 배우 대신 스턴트를 전문으로 대신 역할해주는 배우를 일컫는 스턴트 더블에서 비롯되었다고 한다. 

 

이런 개념을 Mock으로만 알고 있었는데, Test Double에는 Mock말고도 다양한 종류들이 있고, 

Dummy < Fake < Stub < Spy < Mock 순으로 구현이 까다로워 지기 때문에 테스트에 필요한 Test Double이 뭔지 생각해보고, 그에 맞는 것을 선택해서 사용하는 것이 효과적이다. 

 

각 종류에 대한 정의는 https://martinfowler.com/bliki/TestDouble.html 에서 가져왔다.(영어로 되어 있는 부분) 

    • Dummy object Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists. 즉, 테스트를 위해 어떤 객체가 필요하긴 하지만 해당 객체의 내부 기능이 필요하지는 않을 때 사용한다. 그래서 파라미터를 채우기 위해 필요하지만 기능은 비어있어도 되는 경우 Dummy object를 사용한다.
      public class DummyAuthorizer implements Authorizer {
      	@Override
          public Boolean authorize(String username, String password) {
          	return null;
          }
      }

 

    • Fake object Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example). 실제 로직이 구현된 것처럼 동작하지만 내부적으로는 production환경과는 조금 달리 더 간단히 되어있든지 해서 완전히 동일하지는 않은 것.
      public class AcceptingAuthorizerFake implements Authorizer {
      	public boolean authorize(String username, String password) {
          	return username.equals("Bob");
          }
      }​


    • Test stubStubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. 어떤 call이 되었을 때 특정 값을 리턴시키거나 메세지를 output해주거나 하는 식으로 만들어서(특정 상태를 가정한 하드 코딩 식으로) 테스트에 사용하는 것. 그래서 로직에 따른 값의 변경을 테스트 할 수는 없고 미리 특정 상황에 정의된 데이터를 보유하고 해당 상황 호출시를 테스트하고 싶을 때 사용한다.
      public class AcceptingAUthorizerStub implements Authorizer {
      	@Override
          public Boolean authrize(String username, String password) {
          	return true;
          }
      }​

       
    • Test spy  Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent. spy는 stub의 역할을 하면서 추가로 호출시 확인이 필요한 몇 가지를 기록하는 객체이다.
      public class AcceptingAuthorizerSpy implements Authorizer {
      	pulbic boolean authorizeWasCalled = false;
          
          public Boolean authorize(String username, String password) {
          	authorizeWasCalled = true;
              return true;
          }
      }​

 

    • Mock object  Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.
      public class AcceptingAuthorizerVerificationMock implements Authorizer {
      	public boolean authorizeWasCalled = false;
        	
          public boolean authorize(String username, String password) {
          	authorizeWasCalled = true;
              return true;
          }
       	
          public boolean verify() {
          	return authorizeWasCalled;
          }
      }​

 

3) given-when-then 패턴을 사용하자. 

  • given: 테스트에 대한 pre-condition
  • when: 테스트하고 싶은 동작 호출
  • then: 테스트 결과 확인
public void testGetPageHierarchyAsXml() throws Exception {
	givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
 	
    whenRequestIsIssued("root", "type:pages");
    
    thenResponseShouldBeXML();
}

 

4) FIRST 원칙을 생각하며 unit test를 작성하자.

  • Fast: 테스트는 빨리 돌아야 한다. 자주 돌려야 하기 때문이다. 
  • Independent: 각 테스트를 독립적으로 작성한다. 의존하는 코드가 있다면 테스트 실패시 원인을 찾는 것이 어렵다. 
  • Repeatable:테스트는 어떤 환경에서도 반복 가능해야 한다. 
  • Self-Validating: 테스트는 스스로 결과물이 옳은지 그른지 판단할 수 있어야 한다. 그래서 bool 값으로 결과를 내야한다. 
  • Timely: 실제 코드를 구현하기 직전에 테스트 먼저 구현해야 한다. TDD 방법론에 적합한 원칙이지만 실제로 적용되지 않는 경우도 있다.

 

실무에서 모든 구현사항에 대해서 테스트를 짜는 것은 정말 쉽지 않다. 그리고 TDD는 더욱 더 쉽지 않다. 

그래서 완벽하게 테스트를 계획, 구현하는 것은 오히려 생산성을 떨어뜨리고, 새로운 기능을 추가하는데 망설임을 줄 수 있을것 같다. 

 

그러나 테스트의 중요성을 인지하고, 최대한 짜려고 하는 것, 

테스트 자동화를 도입해 두는 것

테스트 가능한 환경를 만들어 두는 것(예를 들어, QA를 위한 production과 유사한 cloud 환경 마련, [Github, CI/CD] Github Actions self hosted runner with own GPUs

은 정말 중요하고 해야할 것이라고 생각한다. 

 

 

 

Reference

 

Test Double 파헤치기

https://speakerdeck.com/guardiola31337/elegant-unit-testing-droidcon-berlin-2016?slide=29