ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 클린 코드 - 단위 테스트
    프로그래밍/방법론 2021. 8. 27. 18:45
    반응형

    요약

    • 테스트 케이스는 현재 아키텍처와 설계를 최대한 깨끗하게 보존하는 열쇠
    • TDD를 무조건 따를 필요는 없다.
    • 테스트 코드는 가독성이 중요하다. 
    • 테스트당 개념/assert 하나씩.
    • FIRST : Fast, Independent, Repeatable, Self-Validating, Timely

     

    90년대는 프로그램이 돌아가는 수준까지만 중요해서 테스트 코드가 그렇게 중요하지 않았다. 근래 우리 프로그래밍 분야는 눈부신 성장을 이루어 테스트 코드를 쉽게 작성할 수 있는 수준까지 도달하였다.

    애자일과 TDD(Test-Driven Development) 덕분에 테스트를 자동화하는 프로그래머들이 이미 많아졌으며 점점 더 늘어나는 추세이다. 테스트와 코드를 하나의 소스 패키지로 공유할 수도 있다. 하지만, 아무리 급해도 제대로 된 테스트 케이스를 작성해야한다는 사실을 놓쳐버리곤 한다. 

     

    전통적인 개발 방법 vs 애자일 개발 방법

    https://velog.io/@hanblueblue/spring-boot-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-2.-TDD 

    위 방식 둘 다 큰 규모의 소프트웨어를 완성하는 개발 방식이다. 

    WaterFall 방식

    • 요구사항이 유동적으로 계속 변하지 않고 순서대로 일이 진행되고 다시 되돌아갈 수 없다.

    애자일 방식

    • WaterFall 테스트 방법과 달리 순차적이지 않고 연속적이지 않다.
    • 애자일 테스트는 1~4주의 짧은 프레임을 가지고 있다.
    • 개발을 할 때마다 테스트코드를 작성하므로 짧게 자주 작성된다. 

     

    TDD 법칙 세 가지 

    쉽게 말해서 선 테스트 작성 후 코드 작성을 말한다. (예시)

    • 첫째 법칙 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
    • 둘째 법칙 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다. 
    • 셋째 법칙 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다. 

    테스트가 실패해야 이 테스트가 검증을 제대로 하고 있는지 알 수 있다. 

    이렇게 테스트 코드를 작성하면 테스트 코드와 실제 코드가 함께 나올뿐더러 테스트 코드가 실제 코드보다 불과 몇 초 전에 나온다. 실제 코드를 사실 상 전부 테스트하는 테스트 케이스가 나온다. 하지만 이런 방대한 테스트 코드는 심각한 관리 문제를 유발한다.

     

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

    테스트 케이스가 없으면 개발자는 자신이 수정한 코드가 잘 도는지 확인할 길이 없다. 테스트 케이스가 없으면 시스템 이쪽을 수정해도 저쪽이 안전하다는 사실을 검증하지 못한다. 그래서 결함율이 높아진다. 의도하지 않은 결함 수가 많아지면 개발자는 변경을 주저한다. 변경하면 득보다 해가 크다 생각해 더 이상 코드를 정리하지 않는다. 그러면서 코드가 망가지기 시작한다. 결국 테스트 케이스도 없고 얼기 설기 뒤섞인 코드에 좌절한 고객과 테스트에 쏟아 부은 노력이 허사였다는 생각만 남는다. 

    하지만 테스트 코드가 깨끗하다면 테스트에 쏟아부운 노력이 허사가 아닐 것이다. 깨끗한 단위 테스트로 코드 수정을 성공할 수 있다. 테스트 코드는 실제 코드 못지 않게 중요하다. 테스트 코드는 사고와 설계와 주의가 필요하다.

     

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

    코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목은 바로 단위 테스트이다. 테스트 케이스가 있으면 변경이 두렵지 않다. 

    테스트 케이스가 있다면 공포가 사라지고 테스트 커버리지가 높을수록 공포는 줄어든다. 실제 코드를 점검하는 자동화된 단위 테스트 케이스는 설계와 아키텍쳐를 최대한 깨끗하게 보존하는 열쇠이다. 테스트케이스가 있으면 변경이 쉬워지기 때문이다. 테스트 케이스가 지저분하면 실제 코드도 지저분해진다. 결국 테스트 코드를잃어버리고 실제 코드도 망가진다. 

    테스트 케이스는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠이다. 테스트 케이스가 있으면 변경이 쉬워진다. 

    따라서 테스트 케이스가 지저분하면 코드를 변경하는 능력이 떨어지며 코드 구조를 개선하는 능력도 떨어진다. 테스트 코드가 지저분할수록 실제 코드도 지저분해진다. 

     

    깨끗한 테스트 코드

    깨끗한 테스트 코드를 만들려면 가독성이 제일 중요하다. 아래 테스트 코드는 이해하기 어렵고 중복 코드가 많다. 

    // SerializedPageResponderTest.java
    public void testGetPageHieratchyAsXml() throws Exception
    {
        crawler.addPage(root, PathParser.parse("PageOne"));
        crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
        crawler.addPage(root, PathParser.parse("PageTwo"));
        request.setResource("root");
        request.addInput("type", "pages");
        Responder responder = new SerializedPageResponder();
        SimpleResponse response =
        (SimpleResponse) responder.makeResponse(
            new FitNesseContext(root), request);
        String xml = response.getContent();
        assertEquals("text/xml", response.getContentType());
        assertSubString("<name>PageOne</name>", xml);
        assertSubString("<name>PageTwo</name>", xml);
        assertSubString("<name>ChildOne</name>", xml);
    }
    
    public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks()
    throws Exception
    {
        WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
        crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
        crawler.addPage(root, PathParser.parse("PageTwo"));
        PageData data = pageOne.getData();
        WikiPageProperties properties = data.getProperties();
        WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
        symLinks.set("SymPage", "PageTwo");
        pageOne.commit(data);
        request.setResource("root");
        request.addInput("type", "pages");
        Responder responder = new SerializedPageResponder();
        SimpleResponse response =
        (SimpleResponse) responder.makeResponse(
            new FitNesseContext(root), request);
        String xml = response.getContent();
        assertEquals("text/xml", response.getContentType());
        assertSubString("<name>PageOne</name>", xml);
        assertSubString("<name>PageTwo</name>", xml);
        assertSubString("<name>ChildOne</name>", xml);
        assertNotSubString("SymPage", xml);
    }
    
    public void testGetDataAsHtml() throws Exception
    {
        crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");
        request.setResource("TestPageOne");
        request.addInput("type", "data");
        Responder responder = new SerializedPageResponder();
        SimpleResponse response =
        (SimpleResponse) responder.makeResponse(
            new FitNesseContext(root), request);
        String xml = response.getContent();
        assertEquals("text/xml", response.getContentType());
        assertSubString("test page", xml);
        assertSubString("<Test", xml);
    }

    위 코드는 투머치 인포메이션이다. 크롤러가 사용하는 pagePath는 이 테스트 코드와 무관하다. responder 객체 생성도 독자가 알 필요가 없다. 

    아래는 이를 리팩토링한 코드이다.

    public void testGetPageHierarchyAsXml() throws Exception{
        makePages("PageOne","PageOne.ChildOne","PageTwo");
        submitRequest("root","type:pages");
        assertResponseIsXML();
        assertResponseContains(
        "<name>PageOne</name>","<name>PageTwo</name>","<name>ChildOne</name>"
        );
    }
    public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception{
        WikiPage page=makePage("PageOne");
        makePages("PageOne.ChildOne","PageTwo");
        addLinkTo(page,"PageTwo","SymPage");
        submitRequest("root","type:pages");
        assertResponseIsXML();
        assertResponseContains(
        "<name>PageOne</name>","<name>PageTwo</name>","<name>ChildOne</name>"
        );
        assertResponseDoesNotContain("SymPage");
    }
    public void testGetDataAsXml() throws Exception{
        makePageWithContent("TestPageOne","test page");
        submitRequest("TestPageOne","type:data");
        assertResponseIsXML();
        assertResponseContains("test page","<Test");
    }

    온갖 잡다한 코드를 없앴다. 각 테스트 코드는 명확히 세 부분으로 나누어진다. Build-Operate-Check. 

    • Build - 테스트 자료를 만든다.
    • Operate - 테스트 자료를 조작한다.
    • Check - 조작 결과가 올바른지 확인한다.

     

    테스트당 assert 하나 

    테스트 함수마다 assert을 하나만 사용해야한다는 학파가 있다. 아래 코드를 보면 확실히 장점이 있다.

    public void testGetPageHierarchyAsXml() throws Exception {
        givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
        whenRequestIsIssued("root", "type:pages");
        thenResponseShouldBeXML();
    }
    public void testGetPageHierarchyHasRightTags() throws Exception {
        givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
        whenRequestIsIssued("root", "type:pages");
        thenResponseShouldContain(
            "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
        )
    }

    함수들 이름을 바꿔 given-when-then의 관례를 사용한 것을 주목한다. 그러면 테스트 코드를 읽기가 쉬워진다. 그런데 중복되는 코드가 많아진다. 그렇다면, @Before에 given/when을 넣고 @Test에 then을 넣는다. 

    때로는 assert 문을 여러개 넣기도 하지만 assert 문의 갯수는 줄여야 좋다.

     

    F.I.R.S.T

    깨끗한 테스트는 아래 다섯 규칙을 따른다.

    • Fast : 테스트는 빨리 돌아야한다. 테스트가 느리면 자주 돌릴 엄두를 못 낸다. 자주 돌리지 않으면 초반에 문제를 고치지 못한다. 
    • Independent : 각 테스트는 서로 의존하면 안 된다. 한 테스트가 다음 테스트가 실행되어야 준비되면 안 된다. 서로에게 의존하면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워진다.
    • Repeatable : 테스트는 반복 가능해야한다. QA 환경, 버스타고 집으로 가는길(네트워크 없을 때)에도 실행 가능해야한다. 환경이 지원되지 않을 때 테스트를 수행하지 못하는 일이 생긴다.
    • Self-Validating : 테스트는 boolean 결과를 내야한다. 성공 아니면 실패여야 한다. 통과여부를 알기 위해 로그 파일을 읽던가 하면 안 된다. 수작업으로 비교하게 해서도 안 된다.
    • Timely : 테스트는 시기 적절하게 시행되어야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다. 실제 코드를 먼저 구현하면 테스트 코드를 만들려면 테스트 코드 작성이 어려워질 수도 있다. 테스트가 가능하도록 실제 코드를 설계해야 한다.

     

     

    Refers

    https://blog.metafor.kr/159

    반응형
Designed by Tistory.