제공 :
한빛 네트워크
저자 : Michael Labriola
역자 : 오윤재
원문 :
Automated testing and you, self-help for the Flex developer: Part 1
소프트웨어 개발자들은 게으른 부류에 속한다. 사실 그렇다. 하지만, 이런 사실이 개발자들로 하여금 일을 더 잘하도록 만들어 주는 것이다. 우리는 일을 할 때, 더 쉽거나 혹은 적어도 더 빠른 방법을 찾고자 하는데 본능적인 욕구를 가지고 있다. 물론 개발자들은 완고한 부류에도 속한다. 그래서 보통 정말 왜 그게 중요한지 몸소 경험해 보지 않는 상황에서는 이해하려고 들지 않는다.
이번 연재 기사에서 다루려고 하려는 것은 이러한 것들이다. 왜 우리 팀에서 테스트 코드를 작성하는지 그 이유와, 그로부터 무엇을 얻을 수 있는지도 이야기 할 것이다. 처음 시작하는 일 또한 간단하고 테스트 코드를 작성하는 것 역시 큰 작업을 필요로 하지 않는다는 것도 보게 될 것이다. 아마도 시작이 어렵지 않기 때문에 여러분의 프로젝트에 시도해 볼 수도 있을 것이다. 일단 시도해 본다면, 나중에라도 개발시 여러분에게 적합한 더 빠르고 쉬운 방법을 찾는데도 도움이 될 것이다.
전제 조건
- Adobe Flex Builder 3
- Flex와 ActionScript에 대한 기초 지식
진실
개발자들은 항상 자신들의 코드를 테스해 본다. 하지만 보통 아주 좁은 범위내에서 한다. 개발자들은 코드의 한 부분이 문제없다고 믿게 될 때까지 그 부분을 바꿔가며 시도해 본다. 그들은 만들어진 애플리케이션을 가지고 작업을 하지만, 매우 복잡한 경우는, 그 부분을 돌려보는 간단한 테스트 프로그램을 만들어 작업하기도 한다. 그 부분의 코드가 잘 작동했을 때, 문제가 발생하지 않는 이상은 그 부분을 그냥 내버려 두고 거의 다시 테스트해보는 일은 없다.
불행하게도, 이 말은 많은 부분의 코드들이 그렇게 작성되고 프로젝트 기간 동안 방치된다는 것을 의미한다. 그럼에도 개발자들은 모든게 여전히 문제없이 잘 돌아가고 있다고 믿는다. 애플리케이션의 다른 부분에 변경이 생기면, 이러한 많은 코드들이 갑작스럽게 문제를 일으키는 경우가 있다. 문제는 개발자나 팀에서 이 부분의 코드가 어떻게 동작하고 있는지 까맣게 잊어버렸을 때쯤 발견된다는 점이다.
접근방법
이번 기사에서는
http://fluint.googlecode.com에서 제공되는 오픈소스인 Fluint(Flex Unit and Integration)프레임워크를 사용한 Unit Testing(단위 테스팅) 실행 방법을 보여줄 것이다. 다음 기사에서는 Fluint를 통한 asynchronous(비동기) 및 integration testing(통합 테스팅)과 Quick Test Prop를 통한 integration testing(통합 테스팅), 마지막으로 code coverage(코드 커버리지)를 다룰 것이다. 하지만 테스트를 하기 위해 어떤 툴을 쓰든 상관없이 대부분의 개념은 어디나 적용이 가능하다.
이번 기사는 특별히 ActionScript를 사용하는 개발자들에 의해 작성된 test (테스트 코드)들에 관해 이야기 한다. 이 테스트들은 때로는 사용자와 상호작용에 의하거나 혹은 상관 없이도 작동하도록 디자인되었다. 코딩 경험이 없는 QA 운영자에 의해 작성된 그러한 테스트는 아니다. 하지만 이 부분도 나중 기사들에서 다루게 될 것이다.
매력적인 점
우리 팀은 테스트 코드를 많이 작성한다. 사실, 한번은 이미 과부하 상태인 일정 가운데 핵심 프로젝트에 필요한 많은 양의 테스트 코드들을 보강하는데 한달 내내를 보낸적도 있다. 몇가지 매우 단순한 이유 때문에 우리는 그렇게 한다.
우리가 수작업으로 할 수 있는 것보다 훨씬 많이 테스트 할 수 있게 해준다.
무의미한 시간이 절대 아니다. 얼마 동안 동작해 보지 않은 코드는 항상 문제의 소지가 있다. 혼자든 아니면 팀의 일원이든 관계없이, 개발자는 진행중 언제든 마감일을 맞추기 위해 몇몇 특별한 기능을 포기한다. 만일 그런 기능들이 프로젝트에서 빠진다면, 개발자의 시간을 잡아먹는 수작업 테스트 작업이 가장 우선 순위가 높다는 것은 이상하지 않는가?
자동화된 테스트 코드들을 만들면, 그것들을 지속적으로 돌려볼 수 있다. 테스트 코드들을 만든 후, 새로 변경된 부분이 다른 부분의 코드에 영향을 미치는지 즉각 알아볼 수 있도록 지속적인 실행이 가능하다.
지속적으로 테스트를 실행하기 위해 QA 팀 사람들은 고용하는 것보다 저렴하다.
자동화된 테스트는 작성하는데 비용이 든다. 하지만 이것은 한번 뿐이고 계속적으로 발생하는 비용이 아니다. 테스트 코드를 작성한 후는 거의 비용이 들지 않고 언제든지 돌릴 수 있다. 이와는 반대로, 수작업 테스팅 작업은 처음에는 비용이 적게 들지만, 매번 테스트 하고자 할 때마다 비용이 다시 발생한다.
버그를 찾고 수정하는데 드는 시간을 줄여준다.
자동화된 테스트 코드들은 계속적으로 돌릴 수 있다. 이 말은 어떤 개발자가 버그를 유발해도 짧은 시간 안에 발견할 수 있다는 뜻이다. 두 가지 매우 중요한 이유가 있다. 첫째, 개발자들은 항상 다음 문제에 대한 해결책을 찾아야 한다. 버그를 발견하기까지 몇주 혹은 몇개월이 걸린다면, 코드를 다시 찾아보고 파악해야 하는데 또다른 노력이 필요하다. 처음 코드를 작성했을 때는 이 문제를 해결하는데 잠깐이면 되었을 것을 수일 혹은 더 걸릴 수도 있다. 둘째는, 버그를 다시 해결하는데 더 오래 걸리면 걸릴수록 더 많은 코드가 다시 수정되어야 할 가능성도 더 커진다.
예를 들어, 내가 수없이 확장되고 굉장히 많이 클래스에 적용된 아주 핵심 기초 클래스에 실수를 했다면, 관련된 모든 클래스들은 지금 중요한 문제가 된 것이다. 이런 부분을 생각해 볼 때, 버그를 잡는 일에 중요한 변경 또는 인터페이스에 수정을 요할 수 있는 가능성은 항상 존재한다. 이건 모든 향후 작업을 곤란하게 만들 수 있다.
뭔가 말썽을 일으키지 않을까 염려하지 않고도, 코드를 수정하거나 다시 작성할 수 있게 해준다.
어떤 작업을 수행하기 위해 더 나은 실행, 혹은 단순히 더 나은 방법을 발견한다고 상상해 보라. 개발자로서 이 전략을 실행에 옮기고 싶어할 것이다. 하지만 만일 코드가 수백 혹은 수만 곳에서 사용되었다면, 어떻게 프로젝트를 망치지 않을 거라고 확신할 수 있는가? 또는 더 쉬운 예로, 거의 사용되지 않아 분명하지 않은 몇몇 클래스들이 (위에서 언급한) 세 가지 중요한 사항들을 피해갈 수 있다고 어떻게 보장하는가? 만일 여러분의 코드가 충분한 테스트를 거친다면, 여러분은 대부분을 고칠 수 있다. 테스트 코드가 여전히 통과되었다면, 코드도 여전히 잘 작동된다. 이것이 밤에 잠을 잘 자는 비법이다.
우리가 작성하는 소프트웨어의 질을 높여준다.
충분히 자동화된 테스트를 거친 코드는 치명적인 버그를 가진 채 배포되는 일이 거의 없게 된다. 애플리케이션은 개발과정에서 수천 번 테스트되게 될 것이다. 대부분의 소프트웨어는 버그가 여전히 존재한다. 하지만 대부분 근본적이고 주목할만한 만큼은 아니다.
궁극적으로 고객들에게 더욱 일 잘하는 개발자로 비춰진다.
우리가 처음부터 잘 동작하는 견고한 코드를 배포했을 때, 우리는 고객들에게 일 잘하는 개발자들로 자리메김 한다. 이것은 다음 번 큰 고객을 상대할 때 우리가 요구할 수 있는 몸값의 범위와 상관 관계가 있다. 여러분이 하는 일을 더 잘 할 수 있게 도와주는 도구들은 항상 소중하게 생각하는게 좋다.
Fluint 설치
Flunit는 testing framework(테스팅 프레임워크)이다. 이것은 ActionScript 소스나 컴파일된 SWC 라이브러리 파일 형태로 다운로드 받아 이용이 가능하다. 여기서는 라이브러리 형태를 사용한다.
1. 새로운 Flex 프로젝트를 생성한다.
2. fluint_v1.swc를 새로 생성한 프로젝트의 libs 폴더로 다운로드한다. fluint_v1.swc는
http://fluint.googlecode.com 사이트의 다운로드 메뉴에서 찾을 수 있다.
3. 위 동일한 주소에서 FlexTestRunner.mxml를 다운받아 새로 생성한 프로젝트의 src 폴더에 저장한다.
4. Flex Builder에서 FlexTestRunner.mxml를 오른쪽 버튼을 눌러 기본(Default) 애플리케이션으로 설정한다.
위의 과정을 마치면, 프로젝트 폴더가 아래 그림처럼 보일 것이다.
[그림 1] Flex 프로젝트에 설치된 Fluint SWC와 테스트 실행기(Test Runner)
fluint_v1.swc는 컴파일된 fluint 프레임워크이다. 이것은 테스트 코드를 작성하기 위한 기본(base) 클래스와 헬퍼(helper) 클래스를 제공한다. FlexTestRunner.mxml는 여러분의 테스트 코드를 실행하고 그 결과를 사용자에게 보여주는 간단한 방법을 제공한다.
Flunit는 이러한 테스트 코드를 실행하는 도구와 각각 테스트한 결과에 대한 성공 또는 실패여부에 대한 정보도 제공한다. 하지만, 여러분이 원하는 어떤 방법이로든 자유롭게 그 정보를 표시하고 이용이 가능하다.
이 시점에 FlexTestRunner를 실행해 볼 수 있다. 애플리케이션이 시작되고 프레임워크가 잘 동작하고 있음을 보여주기 위한 여러 테스트들이 실행될 것이다.
[그림 2] 프레임워크 테스트(test)를 실행하는 샘플 테스트 실행기(Test Runner)
Fluint에서 test들은 test suite과 test case 그리고 test method들로 나눠져 있다. 여러개의 test method 들이 모여서 하나의 test case를 이루고, 다시 여러개의 test case들은 하나의 test suite가 된다. Test Runner가 여러개의 suit들을 실행시키고 그 결과들을 보여준다. 왼쪽편의 디렉토리 목록을 사용해 프레임워크 test suit의 case 및 method들을 탐색해 보거나 윗쪽에 있는 녹색 상태바의 작은 박스모양의 사각형들을 하나 하나 클릭해 볼 수 있다. 상태바의 각각의 녹색 박스는 테스트가 통과되었음을 나타낸다. 빨간색 박스는 실패했음을 나타낸다.
하나씩 개별적으로 test method를 클릭하면, 오른쪽의 trace 창에서는 그것이 성공했는지 실패했는지에 대한 정보를 보여준다. 만일 성공했을 경우는 실행시간이 얼마나 걸렸는지에 관한 정보를 보여주고, 실패했을 경우는 실패하기 이전까지의 stack trace 뿐만 아니라 에러 메시지를 보여준다.
단위 테스트 생성
Unit test(단위 테스트)들은 프로그램의 가장 작은 단위를 테스트하기 위해 고안되었다. 이건 보통 하나의 메서드나 함수를 의미한다. 테스트를 위해 새로운 클래스를 하나 만들어 시작해보자.
테스트가 가능한 클래스 만들기
1. 여러분의 프로젝트 src 디렉토리 안에 helper라는 새로운 폴더를 생성한다
2. helper 폴더 안에 BasicMath.as라는 새로운 ActionScript 클래스를 생성한다.
3. BasicMath 클래스의 AbsouteValue()라는 새로운 메서드를 추가한다. 그 메서드는 Number 타입으로 하나의 인자값을 전달받아 Number를 리턴한다.
public function absoluteValue( value:Number ):Number {
}
주) 여기서 한가지 언급할 것은 여러분은 자신의 개발 방법론을 선택할 수 있다는 점이다. 테스트 기반의 개발방법을 선호하는 사람들은 메서드를 작성하기 전에 먼저 테스트 코드를 작성해야 한다고 믿는다. 그 논리는 매우 간단하다. 즉, 만일 여러분이 하나의 메서드가 어떻게 동작하는가를 검증하는 테스트 코드를 작성한 다음 그 테스트를 통과했다면, 그 메서드는 확실하게 제 기능을 할 수 있다는 것이다. 이건 좋은 철학이다. 하지만 이것은 단지 테스트를 어떻게 하는지 보여주기 위해 준비된 이 기사의 범위를 벗어나는 것이다. 시간을 좀 내서 이러한 개념을 배워보기 바란다.
4. 새로 만든 메서드 안에서는, 입력된 값이 0보다 크거나 같은지를 체크한다. 만일 그렇다면, 단순히 그 값을 리턴한다. 그렇지 않으면, 입력된 값에 -1를 곱한 값을 리턴한다.
public function absoluteValue( value:Number ):Number {
if ( value >= 0 ) {
return value;
} else {
return ( value * -1 );
}
}
이제 여러분은 몇가지를 테스트해 볼 수 있는 클래스와 메서드가 준비된 것이다.
test 메서드와 case 그리고 suite 만들기
1. 여러분의 src 폴더 안에 mathSuit라는 새로운 디렉토리를 생성한다.
2. mathSuit 폴더안에 tests라는 폴더를 생성한다.
3. tests 폴더안에 TestAbs라는 새로운 ActionScript 클래스를 생성한다. 이 클래스는 슈퍼클래스로 net.digitalprimates.fluint.tests.TestCase를 사용한다.
4. TestAbs 클래스 안에 리턴타입이 void인 testPositiveNumber라는 새로운 public 메서드를 생성한다.
5. 그 메서드 안에 BasicMath 타입의 basicMath 변수를 새로 선언하고 그것을 초기화 한다.
6. 다음은 Number 타입의 newValue를 선언하다.
7. 마지막으로, 인자값으로 숫자 5를 넘겨주는 BasicMath의 인스턴스인 absoluteValue 메서드를 호출한다. 반환값을 newValue에 할당한다.
public function testPositiveNumber():void {
var basicMath:BasicMath = new BasicMath();
var newValue:Number;
newValue = basicMath.absoluteValue( 5 );
}
8. 이번 테스트의 마지막 단계로, 값의 검증을 위한 assertion을 만드는 것이다. assertion은 테스트에서 이 지점에 있는 어떤 상태에 대해 프레임워크에 선언을 하는 것이다. 바로 메서드의 마지막 라인에 아래의 코드를 삽입하게 된다.
assertEquals( newValue, 5 );
다시 말해, 테스트가 성공하기 위해서는 newValue 값이 5가 되어야 한다는 것을 의미하고 있다.
‘test’로 시작하는 메서드의 경우, fluint 프레임워크가 기본적으로 이 메서드를 바로 test case의 일부로 시작하려 한다고 알고 있다. TestCase로 부터 확장(상속)되고 적어도 하나의 메서드를 가진 클래스를 생성했다는 것은 여러분이 test case를 위해 첫 단추를 잘 끼웠다는 것을 의미한다. 다음으로, 여러분은 실행을 위해 이 test case를 test suite에 추가해야 한다.
9. 이전에 생성한 mathSuite 디렉토리 안에 슈퍼클래스를 net.digitalprimates.fluint.tests.TestSuite로 지정한 MathSuite라는 새로운 ActionScript를 생성한다.
10. 이 클래스의 맨 위에 여러분이 위에서 생성한 test case를 위해 import 구문을 추가한다.
import mathSuite.tests.TestAbs;
11. MathSuite 클래스의 생성자 안에서 여러분의 TestAbs 클래스의 새로운 인스턴스를 추가하기 위해 TestSuite 슈퍼클래스의 addTestCase() 메서드를 이용한다.
public function MathSuite() {
addTestCase( new TestAbs() );
}
이번 예제에서 MathSuite는 현재 하나의 메서드를 가지고 있는 test case 하나를 포함하고 있다. 하지만 실제 테스트 상황에서 여러분은 수십 혹은 수백개의 메서드들을 각각 가지고 있는 수백개의 teat case를 가지고 있을 수 있다.
12. 테스트 코드를 실행하기 전 마지막 단계로, 새로운 test suite를 테스트 실행기(test runner)의 큐(queue)에 추가한다. 이를 위해 FlexTestRunner.mxml 파일을 열고 startTestProcess() 메서드를 찾는다. 이 메서드는 배열을 생성하고 테스트 실행기의 startTests() 메서드로 그것을 보내는 역할을 담당하고 있다.
13. 현재는 FrameworkSuite의 새로운 인스턴스가 만들어졌고 배열에 추가되었다. 그 배열에 MathSuite의 새로운 인스턴스를 추가하기 위해 아래의 코드를 변경한다.
protected function startTestProcess( event:Event ) : void {
var suiteArray:Array = new Array();
suiteArray.push( new MathSuite() );
testRunner.startTests( suiteArray );
}
주) 이 배열에 MathSuite와 FrameworkSuite를 둘 다 추가할 수도 있다. 이 경우 두 suite들이 함께 실행되고 결과가 각각 보여진다.
14. FlexTestRunner 애플리케이션을 실행한다. 오타없이 잘 입력했으면, 정확히 하나의 테스트가 실행한 후 성공했다는 결과가 보여진다.
Test Case 마무리하기
지금은 test case 안에는 하나의 메서드만 있다. 불행하게도, 간단한 absoluteValue() 메서드를 테스트하는데도 하나의 테스트 메서드만으로는 충분하지 않다. absoluteValue() 메서드 안에 If 구문이 있다. 즉, 값이 0 보다 크거나 같은 경우 값을 리턴하고, 그렇지 않을 경우는 -1을 곱한 값을 리턴한다. 현재는 그 메서드를 다른 건 없이 하나의 방법으로 테스트하고 있다. 실제 상황에서는 하나의 메서드의 모든 가능한 방법에 대한 test가 필요할 것이다.
이러한 문제를 개선하기
1. TestAbs의 test case를 오픈한다.
2. checkNegativeNumber()라는 이름의 새로운 Public 메서드를 이 클래스에 추가한다.
3. testPositiveNumber() 메서드의 기능을 추가한다.
4. 이 메서드 안에서 absoluteValue()에 -5 값을 전달한다. 여러분의 메서드는 아래 샘플과 같다.
public function checkNegativeNumber():void {
var basicMath:BasicMath = new BasicMath();
var newValue:Number;
newValue = basicMath.absoluteValue( -5 );
assertEquals( newValue, 5 );
}
5. 이 메서드 이름은 test란 단어로 시작하지 않는다. 그렇기에 Flunit에 의해서 자동으로 인식되지 않는다. 이 문제를 수정하기 위해서 이 메서드에 [Test]라는 메타데이타를 추가할 수 있다. 이렇게 하는 것은 여러분이 이 메서드를 test로 실행하려 한다는 것을 (프레임워크에) 알려준다.
[Test]
public function checkNegativeNumber():void
6. [Test]라는 메타데이터(metadata)는 추가 기능을 제공하는데, test를 가진 여러분의 메서드가 단순히 시작할 때 이용하지는 않는다. 나중에 사용자 화면에서 이용 가능하도록 test의 설명(description)과 다른 정보를 함께 넘겨준다. 예를들어, 이 test를 나타내는 설명과 잠재적인 버그ID를 넘겨주기 위한 메타터이터(metadata)는 아래와 같다.
[Test(description="This checks negative numbers",bugID="33")]
이러한 구문(syntax)을 이용하면, 여러분은 그 test와 함께 원하는 어떠한 속성들도 함께 넘겨줄 수 있다.
7. 이 시점에서 FlexTestRunner를 동작하면, 통과된 두개의 테스트 결과를 볼 수 있다.
Test Case 설정과 해제하기
현재 test 코드는 잘 작동한다. 하지만 각 메서드에서 BasicMath 클래스의 인스턴스가 제 각각 생성된다. 이렇게 간단한 샘플에서 큰 문제가 되지는 않는다. 하지만 만일 각각의 test 전에 실행을 위한 복잡한 단계가 있다면, test 메서드들 각각을 시작할 때, 이것들을 관리하기가 어렵게 된다.
이런 목적으로 test case는 setUp()과 tearDown()라는 두개의 메서드를 가지고 있다. setUp() 메서드는 각각의 test 메서드가 동작하기 전 수행할 어떤 작업을 포함한다. tearDown() 메서드는 setUp()에서 생성한 객체들을 해제한다.
setUp()과 tearDown()는 각각의 test 메서드를 위해 호출된다. 그래서 이 예제를 사용하면 그 실행 순서는 아래와 같다.
setUp()
testPositiveNumber()
tearDown()
setUp()
checkNegativeNumber()
tearDown()
이러한 메서드들을 사용한 경우, 새로운 test case는 아래와 같을 것이다.
private var basicMath:BasicMath;
override protected function setUp():void {
basicMath = new BasicMath();
}
override protected function tearDown():void {
basicMath = null;
}
public function testPositiveNumber():void {
var newValue:Number;
newValue = basicMath.absoluteValue( 5 );
assertEquals( newValue, 5 );
}
[Test(description="This checks negative numbers",bugID="33")]
public function checkNegativeNumber():void {
var newValue:Number;
newValue = basicMath.absoluteValue( -5 );
assertEquals( newValue, 5 );
}
다음 단계들
이번 기사에서는 개발자의 unit test(단위 테스트)들을 만드는 기본적인 것들에 대해 소개했다. 하지만 기본적인 단위 테스트들은 빙산의 일각에 불과하다. 다음 기사에서는 asynchronous unit, integration and functional 테스팅을 다루게 될 것이다. 그 동안
http://fluint.googlecode.com에서 Fluint에 대한 고급 특징들을 배워보기 바란다.