'소프트웨어 테스팅' 검색 결과 6건

  1. 2008.12.23 Selenium 한글화 및 인간화(humanize) (10)
  2. 2008.12.05 JSSpec2 개발 현장 (2)
  3. 2008.11.26 JSSpec으로 setInterval 테스트 하기 (4)
  4. 2008.11.05 객체지향적 구구단 (3)
  5. 2008.04.25 3시간 작업한 코드를 revert 하면서.
  6. 2008.04.23 // 테스트 주도 개발이란 (2)
인수테스트(Acceptance Test. 이하 AT) 자동화 도구 중 FIT(Framework for Integrated Test)라는 것이 있습니다. Ward Cunningham의 작품이죠.


MS-Word나 Excel 등을 이용해서 위와 같이 표 형식으로 테스트 문서를 만들면 이 문서가 자동으로 실행되고 테스트 성공 여부를 색깔로 알려주는 방식입니다. 보면 아시겠지만 개발자가 아닌 사람들도 비교적 쉽게 읽고 쓸 수 있게 되어 있습니다.

FIT의 장점은 고객(혹은 기획자/디자이너/마케터/관리자 등 비개발 직군 사람)과 개발자 사이의 대화와 협업을 유도하기 적절한 도구라는 점입니다. 자동화 자체도 중요하지만, 이 도구를 매개로 직군간 대화가 활성화 된다는 점이 더 중요할 수도 있습니다.

뭐 하여간, FIT의 철학을 계승한 테스트 자동화 도구 중에 Selenium이라는 것이 있습니다. 웹을 위한 AT 자동화 도구입니다. Selenium에서 작성된 문서는 대충 이렇게 생겼습니다:


좀 노력해서 읽어보면 대충 무슨 말인지 알겠는데 아무래도 그리 편하게 읽히지는 않죠. 우선 명령어들이 영어죠. 더 큰 문제는 두번째 칼럼에 종종 CSS 셀렉터나 ID나 XPath 등이 나오는데 이게 좀 개발자스럽다는 겁니다.

그래서, 명령어를 한글화하고, 두번째 칼럼(locator)을 인간화(humanize)하면 좋겠다는 생각이 들었어요. 회사에서 하고 있는 프로젝트에 적용하면 기획자와 개발자 사이의 대화를 좀 도울 수 있지 않을까 싶어서 조금씩 추진(?)하고 있습니다(user-extensions.js 를 수정하여).

지금까지 작업한걸 적용해보면, 대충 아래와 같은 테스트를 작성할 수 있게 됩니다:


한결 보기 좋죠?

두번째 칼럼(locator) 부분이 위와 같이 깔끔해지려면 form 태그 내의 label 등이 잘 작성되어 있고, img 에는 title 속성이 적절히 붙어 있어야 하는데, 이게 또 일반적으로 권장되는 마크업(접근성이나 의미 등을 고려할 때) 방식과 같죠.

좀 시간을 투자할 가치가 있을 것 같아요. 대충 마무리되면 공개하도록 하겠습니다.

(JSSpec2에 이어 이것도 개봉박두 ㅎㅎ)
신고
JSSpec 다음 버전을 개발하고 있습니다.

한쪽 모니터엔 이클립스, 다른쪽 모니터엔 각종 호스트 환경들(Windows Script Host, Rhino, IE6, IE7, FF3, Safari3, Chrome, Opera9)이 띄워진 화면입니다:


크게 봐서 목표는 두 가지 입니다:
  • 다양한 DSL 지원(TDD, BDD, Full-stack BDD?)
  • 서버측 JS 등 콘솔 기반 호스트(Rhino, Windows Script Host, Flash/Flex 등) 지원

개봉박두 --;
신고
JSSpec 관련 메일을 일주일에 몇 통 정도 받습니다. 가장 많이 받는 질문 중 하나는(국내외를 막론하고) 바로
JSSpec에서 setInterval()이나 setTimeout()이 포함된 로직을 테스트하려면 어떻게 하나요?
입니다. 메일로 짧게 답하기가 좀 힘들기도 하고, 성의가 부족해보이기도 하고 그래서... 이 주제로 짧은 글을 하나 쓰려고 합니다.


1. 초간단 요구사항 및 초기구현

화면에 숫자 10 이 나타나고, 이게 0.1초1 씩 감소하다가 0 이 되면 멈추는 그런 애플리케이션을 만든다고 칩시다. 다음은 counter.html 입니다 (doctype 생략):
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>Counter</title>
<script type="text/javascript" src="counter.js"></script>
<script type="text/javascript">// <![CDATA[
    window.onload = main;
// ]]></script>
</head>
<body>
    <div id="counter">10</div>
</body>
</html>
다음은 counter.js 입니다:
var counter = 0;
var handle = null;

function main() { startCountdown(); }

function startCountdown() {
    counter = 10;
    handle = setInterval(function() { countdown(); }, 100);
}

function countdown() {
    counter--;
    var element = document.getElementById('counter');
    element.innerHTML = counter;
   
    if(counter === 0) clearInterval(handle);
}
간단한 코드이므로 설명은 생략합니다. 스팩을 먼저 만들고 구현을 하면(즉, BDD 혹은 TDD를 하면) 더 쉽겠지만, 실제 상황과 유사하게 하기 위해서 스팩 없이 구현을 먼저 했습니다. Michael Feather의 명저서 "Working Effectively with Legacy Code(레거시 코드 활용 전략이라는 제목으로 번역되었습니다)"에 의하면, 위 코드는 바로 레거시 코드 입니다.
To me, legacy code is simply code without tests. ... Code without tests is bad code. It doesn't matter how well written it is; it doesn't matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don't know if our code is getting better or worse. --pxvi

저는 테스트 케이스가 없는 코드를 레거시 코드로 봅니다. ... 테스트 없는 코드는 나쁜 코드입니다. 얼마나 잘 짰는지는 중요치 않아요. 아무리 예뻐도, 아무리 객체지향적이어도, 아무리 캡슐화가 잘 되어 있어도 소용 없습니다. 테스트가 있으면 빠르고 검증가능한 방식으로 코드의 행위를 수정할 수 있습니다. 테스트가 없으면 코드가 좋아지고 있는지 나빠지고 있는지 알 방법이 없지요.
이제, 이 레거시 코드를 좋은 코드로 고쳐봅시다.


2. 안도감을 느낄 수 있을만큼의 스팩

너무 많지도 않고 너무 적지도 않은 딱 적절한 양의 스팩을 달고 싶습니다. 다른 말로 하자면 "안도감을 느끼며 개발을 할 수 있을만큼의 스팩"입니다.

뭐 정의 자체가 애매하다보니 사람마다 그 기준이 다를 수 있겠습니다만, 저는 아래 두 가지 로직을 커버할 수 있는 정도면 안도감이 느껴질 것 같습니다:
  • countdown()이 호출되면 div#counter 의 값이 1 씩 감소하는가
  • countdown()0.1초 간격으로 호출되는가

이제 하나씩 공략해 봅시다.


3. countdown()이 호출되면 div#counter 의 값이 1 씩 감소하는가

다음은 스팩입니다. body 태그 안쪽만 옮겼습니다.
<div id="counter">10</div>

<script type="text/javascript">// <![CDATA[
describe('Counter', {
    'countdown()이 호출되면 카운터가 1 감소되어야 한다': function() {
        var element = document.getElementById('counter');
        counter = 5;
        countdown();
        value_of(element.innerHTML).should_be('4');
    }
});
// ]]></script>
일단, 전역변수인 counter5로 강제로 설정하고, countdown() 함수를 호출합니다. 그 결과로 div#counterinnerHTML4로 변했는지를 확인합니다.

스팩을 짜놓고 보니까 좀 지저분합니다. 전역 변수(counter)를  하나 제거하면 좀 나아질 것 같군요.
describe('Counter', {
    'countdown()이 호출되면 카운터가 1 감소되어야 한다': function() {
        var element = document.getElementById('counter');
        element.innerHTML = '5';
        countdown();
        value_of(element.innerHTML).should_be('4');
    }
});
위 스팩이 통과하려면 counter.js를 다음과 같이 수정해서 전역 변수 counter를 모두 제거해야 합니다:
var handle = null;

function main() { startCountdown(); }

function startCountdown() {
    handle = setInterval(function() { countdown(); }, 100);
}

function countdown() {
    var element = document.getElementById('counter');
    var counter = Number(element.innerHTML) - 1;
    element.innerHTML = counter;
   
    if(counter === 0) clearInterval(handle);
}
한편, div#counter 엘리먼트도 어디서든 접근할 수 있다는 점에서 전역 변수라고 볼 수 있겠습니다. countdown() 함수가 이 엘리먼트에 의존하고 있기 때문에 아래의 HTML이 테스트 코드와 프로덕션 코드 두 곳에 중복으로 나타나죠:
<div id="counter">10</div>
스팩을 아래와 같이 수정하면 좋겠습니다:
describe('Counter', {
    'countdown()이 호출되면 카운터가 1 감소되어야 한다': function() {
        var element = {innerHTML: '5'};
        countdown(element);
        value_of(element.innerHTML).should_be('4');
    }
});
countdown() 함수가 element를 인자로 받도록 수정하고, 가짜 엘리먼트를 하나 만들어서 넘겨주었습니다. 이 방식은 countdown() 함수가 element의 innerHTML 속성을 이용할 것이라는 가정을 하고 있다는 점에서 좋지 않습니다만(테스트가 구현 방식에 종속됨), 기존 방식(전역 변수에 의존)보다는 좋다고 판단하였습니다. "best way"는 아니지만 "better way"인거죠.

위 스팩이 통과하려면 counter.js는 아래와 같이 바뀌어야 합니다:
function startCountdown() {
    handle = setInterval(function() {
        var element = document.getElementById('counter');
        countdown(element);
    }, 100);
}

function countdown(element) {
    var counter = Number(element.innerHTML) - 1;
    element.innerHTML = counter;
    if(counter === 0) clearInterval(handle);
}
이 정도로 하고 다음 스팩을 추가하겠습니다.


4. countdown()이 0.1초 간격으로 호출되는가

"countdown()이 0.1초 간격으로 호출되는가"라는 질문은 달리 표현하자면, "0.1초가 지나면 countdown()이 호출되는가"입니다. 이제 드디어 본론에 해당하는 내용이 나왔습니다.

대체 "0.1초가 지나면"이라는 것을 어떻게 테스트하면 좋을까요? JSSpec에 아래와 같은 가상의 기능이 추가되어서 0.1초를 "기다릴 수 있으면" 될까요?
    'countdown()이 0.1초 간격으로 호출되어야 한다': function() {
        // 여기에서 뭔가를 수행하고

        wait_for(100); // 0.1초(100msec)를 멈춰서 기다린 후

        // 여기에서 결과를 확인한다
    }
위와 같은 방식이 기술적으로 가능한지 여부를 떠나서, 이는 좋은 단위 테스트라고 볼 수 없습니다. 만약 요구사항이 0.1초가 아니라 한 시간이었다면, 위 테스트를 수행하는데 한 시간이 걸린다는 얘기인데, 이렇게 되면 원하는 때에 피드백(즉, 테스트 성공 여부에 대한 확인)을 받을 수가 없게 됩니다.

요약하자면, "0.1초가 지난 후 어떻게 되는가"를 테스트하기 위해 실제로 0.1초를 기다려야 하는 방식은 적절치 않습니다.

좀 더 적절한 방법은 0.1초를 가짜로 흘려보내는 것입니다. 자바스크립트의 Date 클래스를 다음과 같이 확장하고, 애플리케이션의 모든 코드에서 new Date() 대신에 Date.get()을 쓰도록 수정할 것을 권장합니다:
Date._preset = -1;
Date.preset = function(msec) {Date._preset = msec;};
Date.pass = function(msec) {
   Date._preset = Date._preset == -1 ? msec : Date._preset + msec;
};
Date.get = function() {
   return Date._preset == -1 ? new Date() : new Date(Date._preset);
};
preset() 함수는 시간을 임의로 설정하여 멈추어놓기 위해 사용됩니다. pass() 함수는 시간을 가상으로 흘려보냅니다. get() 함수는 현재 시간을 얻어옵니다. get() 함수가 반환하는 값은 preset() 혹은 pass()가 사용되지 않았다면 실제 시스템 시간이지만, preset()이나 pass()가 사용되었다면 가짜 시간입니다. 위 코드는 실제 프로덕션에서도 사용될 코드이므로 스팩이 아닌 counter.js에 추가합니다. 이제 아래 스팩을 추가해봅시다:
    '0.1초가 지나면 countdown()이 호출되어야 한다': function() {
        // countdown 바꿔치기
        var backup = countdown;
        var called = false;
        countdown = function() { called = true; };

        try {
            Date.preset(0); // 시간 고정
            updateIfTimePassed();
            value_of(called).should_be(false);
           
            Date.pass(100); // 0.1초 흘리기
            updateIfTimePassed();
            value_of(called).should_be(true);
        } finally {
            // countdown 복원
            countdown = backup;
        }
    }
좀 길군요. 하나씩 설명을 하겠습니다.

첫째, updateIfTimePassed() 함수가 추가되었습니다. 이 함수가 하는 일은 자신이 마지막으로 호출된 후로 0.1초 이상이 흘렀으면 countdown() 함수를 호출해주는 것입니다.

둘째, 이번에 작성한 스팩은 0.1초 간격으로 countdown() 함수가 호출되는가를 확인하기 위한 것이지 countdown() 함수가 제대로 작동되는가를 확인하는 것이 아닙니다. 다른 말로 하자면 countdown() 함수가 제대로 작동하건 안하건 0.1초 간격으로 호출만 된다면 이 스팩이 깨지지 않아야 합니다. 따라서, countdown() 함수를 가짜로 대체하고 다시 복구하는 코드(변수 backup이 사용되는 부분들)가 들어 있습니다.

셋째, 조금 전에 Date 클래스에 추가한 메서드들을 사용하여 시간을 고정시킨 후 가짜로 흘려보내고 있습니다. 이렇게 하면 이 테스트는 실제로 0.1초를 보내지 않고 순식간에 수행됩니다.

이번에는 이에 따른 프로덕션 코드의 수정입니다:
var updatedAt = null;

function startCountdown() {
    handle = setInterval(updateIfTimePassed, 10);
}

function updateIfTimePassed() {
    if(!updatedAt) updatedAt = Date.get();
   
    var now = Date.get();
    var timePassed = now - updatedAt >= 100;
    if(timePassed) {
        var element = document.getElementById('counter');
        countdown(element);
        updatedAt = now;
    }
}
변화되지 않은 부분들 - main(), countdown() - 은 생략하였습니다. 변화된 부분은 다음과 같습니다.

첫째, startCountdown 내부의 setInterval이 updateIfTimePassed를 0.1초 간격이 아닌 0.01초 간격으로 호출하고 있습니다. 0.01초는 대략 "시스템에 무리를 주지 않는 한도 내에서 최대한 빈번한 간격"입니다. 이렇게 수정하자 이 메서드는 말 그대로 카운트다운을 시작하는 일만 하게 되었고, 카운트다운의 간격 등에 대해서는 아무런 간섭도 하지 않게 되었습니다.

둘째, updateIfTimePassed() 함수가 새로 만들어졌습니다. 이 함수는 자신이 마지막으로 호출된 이후로 0.1초 이상이 지나면 countdown() 함수를 호출하도록 되어 있습니다.

셋째, 이 과정에서 전역 변수 updatedAt이 추가되었습니다. 좋지 않지만 잠시 참고 가보도록 하겠습니다.

Date 클래스를 확장하는 방식에 대해서 조금 더 부연설명이 필요하신 분은 유닛테스트에서 시각과 시간이라는 글을 참고해주세요. 자바를 기준으로 설명하고 있지만 결국 같은 방식입니다.


5. 리팩토링

원하는 스팩을 두 개 만들었으니 이제 좀 마음놓고 리팩토링을 해야 합니다. 마음에 안드는 부분들이 많습니다.

첫째, 0.1초 간격으로 무언가를 호출하는 로직(updateIfTimePassed 함수)이 카운트다운을 수행하는 로직(countdown 함수)과 엉켜있습니다. 게다가 countdown 함수에 필요한 인자를 넘겨주기 위해서 DOM에도 의존하고 있습니다.

둘째, 전역 변수가 두 개(handle, updatedAt) 있습니다. 그리고 DOM 이 숨은 전역 변수 역할을 하고 있기도 합니다. 이게 무슨 말이냐하면, 전역 변수라는게 나쁜 이유는 어디에서든 접근할 수 있기 때문에, 코드의 여러 지점이 이 전역 변수를 중심으로 엮인다는 점입니다. 그렇게 본다면 div#counter 라는 엘리먼트가 여기저기에서 쓰이면서 의존성을 만들어내고 있다는 점에서 숨은 전역  변수입니다. 예전에 박응주님이 자바의 ThreadLocal이나 WebWork의 ActionContext 등도 전역 변수이다라고 말씀하신 것과 같은 맥락입니다.

(저는 예전에 위 두 가지 문제를 해결하기 위해 Timer 혹은 Scheduler라는 클래스를 만들었었는데 아주 만족스러웠습니다.)

셋째, 스팩 자체도 깔끔하지가 않습니다. 가짜 객체(element, countdown)를 만들어서 이런저런 의존성을 끊어주어야만 테스트간 격리(test isolation)가 이루어진다는 것은 결국 좋지 못한 설계로 인해 생긴 문제에 다름 아닙니다. 위에서 언급한 두 가지 문제를 해결하면 스팩도 더 깔끔해지겠죠. 원래 테스트하기 쉬운 코드일수록 설계가 좋은 코드라는 말이 있습니다. 그 반대도 성립합니다. 설계가 좋은 코드일수록 테스트하기가 쉽기도 합니다. 역시나 가장 좋은 방법은 테스트(혹은 스팩)를 먼저 만들고나서 설계를 하는 것(즉, TDD 혹은 BDD를 하는 것)입니다.

이제 퇴근을 해야겠으니 이런 부분들은 미해결로 남겨놓겠습니다. ^^;


PS - 요약 및 일반화된 결론

setInterval 을 테스트하는 문제는 사실 더 큰 문제의 구체적 사례일 뿐입니다. 문제를 더 일반화하면 이렇게 됩니다:
제어하기 힘들거나 비용이 많이드는 외부 시스템(시스템 타이머, 디스크, 네트워크, 외부 라이브러리, 프레임워크의 노출된 인터페이스 등등)과 엮인 코드를 어떻게 테스트할 것인가.
이 문제에 대한 일반해는 InsertTestableLayer를 참고하시기 바랍니다.

그런데, TestableLayer라는 것을 넣으면 테스트가 가능해진다는 점 말고 대체 뭐가 좋아지는걸까요. 위에서 설계가 좋아진다고 쓰기는 했는데 구체적으로 뭐가 어떻게 좋아진다는 것일까요. 뭐 여러가지 주절주절 설명할 수 있겠지만... 자바스크립트의 경우는 이렇습니다:

비즈니스 로직(0.1초 간격으로 1씩 카운트다운)에 해당하는 코드를 전혀 수정하지 않고 다른 호스트 환경(host environment - Rhino, jslibs, ASP, JScript.NET, Silverlight, Flash/Flex 등)에서 그대로 사용할 수 있게 됩니다. 이것도 또한 반대로 얘기할 수 있는데, 코드를 전혀 수정하지 않고 다른 호스트 환경에서 사용할 수 있게 된다면 제어하기 힘든 외부 시스템에 대한 의존이 제거되었다고 볼 수 있습니다.

신고
아래 글들에서 구구단 얘기좀 쓰다보니 갑자기 불현듯 생각이 나서 하나 더 씁니다. ㅎㅎ

객체지향적 구구단이라거나 객체지향적 Hello World에 대한 이야기를 간혹 보게 되는데 과연, 객체지향적으로 잘 설계된 구구단이란게 뭘까요?

아니, 객체지향이고 뭐고를 떠나서, 잘 설계된 구구단이란 뭘까요?

Uncle Bob이 "Agile Software Development"라는 책에 쓴 구절을 인용하는게 좋겠습니다:
(발번역입니다 --; ) LSP에서 중요한 결론을 한 가지 얻을 수 있다: 모델만 따로 보아서는 결코 적절성을 논할 수 없다. 해당 모델을 사용하는 코드의 입장에서 보았을 때에만 적절성을 이야기할 수 있는 것이다.

(중략)

특정 설계가 적절한가 그렇지 않은가에 대해서 그 설계만 놓고서는 알 수 없다. 해당 설계를 사용하는 측으로부터 도출되는 타당한 가정들에 기반하여 평가해야 한다. (단위 테스트가 이러한 가정들을 명시적으로 선언하는 역할을 한다. 이는 TDD의 또 다른 장점이다)

The Liskov Substitution Principle leads us to a very important conclusion: A model, viewed in isolation, cannot be meaningfully validated. The validity of a model can only be expressed in terms of its clients.

(...omitted...)

When considering whether a particular design is appropriate or not, one cannot simply view the solution in isolation. One must view it in terms of the reasonable assumptions made by the users of that design(Often you will find that those reasonable assumptions are asserted in the Unit Tests written for the base class. Yet another good reason to practice Test Driven Development).

--p116

그럼 다시. 구구단을 최대한 잘 설계하라고 할 때 어떠한 답을 내놔야 할까요? 저라면 이렇게 짜겠습니다:
public class Gugu {
    public static void main(String args[]) {
        for (int i = 2; i <= 9; i++)
            for (int j = 1; j <= 9; j++)
                System.out.println( i + " * " + j + " = " + (i * j) );
    }
}

스스로 평가를 해보자면:
  • 상속성: 모듈이 하나라 상속 필요 없음
  • 캡슐화: 모듈이 하나라 캡슐화 필요 없음
  • 은닉: 모듈가 하나라 숨길 필요 없음

많은 사람들이 제일 중요한 덕목으로 쳐주는 재활용성(ㄷㄷㄷ)의 측면에서 보자면:
  • 재활용성: 어디에 "다시 쓰일지(re-use)" 예측할 수 있는 정보가 없으므로, 가장 단순하게 작성하였음. 코드가 짧고 명확하니 최소한의 수정비용으로 최대한 다양한 곳에 활용될 수 있음.

이렇다고 생각합니다.

신고
퇴근 전 3시간 동안 작업한 코드를 revert 했습니다. 하지만 3시간 동안 쌓인 spec은 지우지 않고 남겨뒀어요.

남은(혹은 얻은) 것은?
  • 새 IE 버그에 대한 지식
  • 설계 인사이트
  • 각종 상황에 대한 예제들(spec)

잃은(혹은 버린) 것은?
  • 버그를 피하기 위해 adhoc하게 넣었던 지저분한 코드 몇 Kbytes
신고
< Newer     Older >

티스토리 툴바