개념: 테스트 우선 디자인
이 가이드라인은 테스트 우선 디자인에 대해 설명합니다. 코드를 작성하고 테스트하기 전에 우선 테스트 스크립트를 작성하여 테스트 우선 디자인을 적용합니다. 테스트 대상이 없을 때까지 이 기법을 계속합니다.
관계
관련 요소
기본 설명

소개

유스 케이스 실현(realization), 디자인 모델 또는 클래스류 인터페이스와 같은 디자인 중간 산출물을 포함하여 다양한 중간 산출물의 정보를 사용하여 테스트 디자인을 작성합니다. 컴포넌트를 작성한 후에 테스트를 실행합니다. 소프트웨어 디자인 중간 산출물이 잘 작성된 후 테스트를 실행하기 바로 전에 테스트 디자인을 작성하는 것이 일반적입니다. 다음 그림 1은 예제를 표시합니다. 여기서 테스트 디자인은 구현이 거의 끝나갈 때 시작합니다. 테스트 디자인은 컴포넌트 디자인의 결과에 의존합니다. 구현에서 테스트 실행으로의 화살표는 구현이 완료될 때까지 테스트를 실행할 수 없음을 나타냅니다.

라이프사이클 다이어그램의 테스트 디자인 위치

그림 1: 전통적으로 테스트 디자인은 라이프사이클의 뒷부분에 수행됨

그러나 이 방법을 따를 필요는 없습니다. 테스트 실행은 컴포넌트가 구현될 때까지 기다려야 하지만 테스트 디자인은 미리 수행할 수 있습니다. 디자인 중간 산출물이 완료된 직후에 테스트 디자인을 수행할 수 있습니다. 여기 표시된 대로 컴포넌트 디자인과 병행하여 테스트 디자인을 수행할 수도 있습니다.

테스트 우선 디자인 다이어그램

그림 2: 테스트 우선 디자인이 소프트웨어 디자인과 동시에 테스트 디자인을 생성함

이 방법으로 테스트 노력을 "거꾸로(upstream)" 이동하는 것을 보통 "테스트 우선 디자인"이라고 합니다. 테스트 우선 디자인의 이점은 무엇입니까?

  1. 소프트웨어를 디자인할 때는 아무리 주의를 기울여도 실수를 하게 됩니다. 관련 사실을 놓칠 수 있습니다. 또는 특정 대안을 알기 어렵게 하는 특정 사고 습관이 있을 수 있습니다. 또는 단지 피곤해서 무언가를 간과할 수 있습니다. 다른 인원에게 디자인 중간 산출물을 검토하도록 하면 도움이 됩니다. 다른 인원은 당신이 놓친 사실을 알고 있거나 간과한 내용을 볼 수 있습니다. 이러한 인원들이 사용자와 다른 관점을 가지는 것이 가장 좋습니다. 디자인을 다르게 검토함으로써 놓친 내용을 볼 수 있습니다.

    경험에 따르면 테스트 관점이 효과적인 것으로 증명되었습니다. 테스트 관점은 매우 구체적입니다. 소프트웨어 디자인 과정에서 특정 필드를 "현재 고객의 직위를 표시"한다고 여기고 별 생각 없이 다음으로 이동하기 쉽습니다. 테스트 디자인 과정에서, 특별히 해군에서 퇴역한 후 법학 학위를 취득한 고객이 "Lieutenant Morton H. Throckbottle(Ret.), Esq"라고 불리기를 원하는 경우 해당 필드에 표시할 내용을 결정해야 합니다. 이 사람의 직위는 "Lieutenant"입니까 아니면 "Esquire"입니까?

    그림 1과 같이 테스트 실행 직전까지 테스트 디자인을 미루면 비용이 낭비될 것입니다. 어떤 테스터가 "있지, 해군에서 퇴역한 이 남자를 알아..."라고 말하고 "Morton" 테스트를 작성하고 문제를 발견하는 테스트 디자인까지 소프트웨어 디자인의 실수는 파악되지 않습니다. 이제 부분적 또는 전체적으로 전체 구현을 다시 작성해야 하고 디자인 중간 산출물을 갱신해야 합니다. 구현을 시작하기 전에 문제를 파악하면 비용이 적게 듭니다.

  2. 일부 실수는 테스트 디자인 이전에 발견될 수 있습니다. 대신 이러한 실수는 구현자에 의해 발견됩니다. 이 방법도 좋지 않습니다. 디자인 구현 방법에서 해당 디자인의 결과로 초점을 전환하는 동안 구현은 천천히 정지하게 됩니다. 같은 사람이 구현자 및 디자이너 역할을 수행하는 경우에도 혼란을 일으키고, 다른 인원이 각 역할을 수행하는 경우에는 더욱 큰 혼란을 일으킵니다. 이 혼란의 방지는 테스트 우선 디자인이 효율성을 개선하는 다른 방법입니다.

  3. 테스트 디자인은 다른 방법, 즉 디자인을 분명하게 하는 방법으로 구현자를 돕습니다. 구현자에게 디자인의 의미에 대한 의문이 생기면 테스트 디자인을 원하는 동작의 특정 예제로 사용할 수 있습니다. 이를 통해 구현자의 잘못된 이해로 인한 버그가 줄어듭니다.

  4. 구현자가 의문을 갖지 않는 경우에도 적은 수의 버그가 있습니다. 예를 들어 모호한 부분이 있어 디자이너는 무의식적으로 한 방향으로 해석하고 구현자는 다른 방향으로 해석하는 경우가 있을 수 있습니다. 구현자가 디자인 작업 뿐만 아니라 테스트 케이스에서 컴포넌트가 수행할 동작에 대한 특정 지침 작업을 수행하고 있으면 컴포넌트는 실제로 필요한 내용을 수행할 가능성이 많습니다.

예제

다음은 테스트 우선 디자인의 이해를 도울 수 있는 예제입니다.

회의실을 할당하는 이전 "비서에게 문의" 메소드를 바꾸는 시스템을 작성하고 있습니다. MeetingDatabase 클래스의 메소드 중 하나는 getMeeting이라고 하며, 이 메소드에는 다음 서명이 있습니다.

  Meeting getMeeting(Person, Time);

인원 및 시간을 지정하면 getMeeting은 해당 인원이 해당 시간에 참석하도록 계획된 회의를 리턴합니다. 해당 인원에게 아무 스케줄도 없으면 특수 Meeting 오브젝트 unscheduled를 리턴합니다. 일부 직접 테스트 케이스는 다음과 같습니다.

  • 인원이 지정된 시간에 아무런 회의에도 참석하지 않습니다. unscheduled 회의가 리턴됩니까?
  • 인원이 해당 시간에 회의에 참석합니다. 메소드가 올바른 회의를 리턴합니까?

이러한 테스트 케이스는 흥미롭지는 않지만 결국 시도해야 합니다. 언젠가 실행될 실제 테스트 코드를 작성하여 지금 테스트 케이스를 작성할 수도 있습니다. 첫 번째 테스트의 Java 코드는 다음과 같습니다.

      // if not in a meeting at given time,     // expect to be unscheduled.     public void testWhenAvailable() { Person fred = new Person("fred"); Time now = Time.now(); MeetingDatabase db = new MeetingDatabase(); expect(db.getMeeting(fred, now) == Meeting.unscheduled);     }

그러나 보다 흥미로운 테스트 아이디어가 있습니다. 예를 들어 이 메소드는 일치를 검색합니다. 메소드로 검색할 때마다 둘 이상의 일치를 찾는 경우 수행해야 하는 작업을 질문하는 것이 좋습니다. 이 경우는 "한 사람이 한 번에 두 회의에 참석할 수 있습니까?"라고 묻는 것입니다. 불가능해 보이지만 비서에게 이런 경우를 질문하면 의외의 사실이 밝혀질 수 있습니다. 일부 간부는 한 번에 두 회의에 참석하도록 계획되는 경우가 많은 것으로 드러납니다. 간부의 역할은 회의에 나타나서 짧은 시간 동안 "직원들을 결속"시킨 후 이동하는 것입니다. 해당 동작을 수용하지 않는 시스템은 적어도 일부가 사용되지 않은 채로 남게 됩니다.

이 예제는 분석 문제점을 발견하는 구현 레벨에서 완료된 테스트 우선 디자인입니다. 이에 대해 주의해야 할 몇 가지 사항이 있습니다.

  1. 올바른 유스 케이스 정의 및 분석으로 이미 이 요구사항을 발견한 것으로 가정합니다. 이런 경우 문제점이 "거꾸로(upstream)" 진행하는 것을 막고 getMeeting이 다르게 디자인되었을 것입니다. (이는 회의를 리턴할 수 없고 회의 세트를 리턴해야 합니다.) 그러나 분석은 항상 일부 문제점을 놓치므로 배치 이후보다 구현하는 동안 문제점을 발견하는 것이 좋습니다.

  2. 많은 경우에 디자이너 및 구현자는 이런 문제점을 발견하는 도메인 지식을 갖고 있지 않으며, 비서에게 질문할 기회나 시간이 없습니다. 이런 경우 getMeeting에 대한 테스트를 디자인하는 인원은 "두 회의를 리턴해야 하는 경우가 있는가?"라고 묻고, 잠시 생각한 후 없다고 결론을 내립니다. 따라서 테스트 우선 디자인은 모든 문제점을 발견하지 못하지만 올바른 유형의 질문을 한다는 사실만으로 문제점을 찾을 기회가 증가합니다.

  3. 구현 동안 적용되는 동일한 테스트 기법의 일부가 분석에도 적용됩니다. 테스트 우선 디자인은 분석가에 의해 수행될 수도 있지만 이는 이 페이지의 주제가 아닙니다.

세 가지 예제 중 두 번째 예제는 난방 시스템의 상태 차트 모델입니다.

HVAC 상태 차트

그림 3: HVAC 상태 차트

일련의 테스트가 상태 차트의 모든 연결 화살표를 순회합니다. 한 테스트는 대기 시스템으로 시작하여, 너무 더움 이벤트를 삽입하고, 냉방/실행 중 상태 중에 시스템에 오류가 발생하고, 실패를 해결하고, 다른 너무 더움 이벤트를 삽입한 후 시스템을 다시 대기 상태로 실행합니다. 이 테스트는 모든 연결 화살표를 실행하지 않으므로 추가 테스트가 필요합니다. 이러한 유형의 테스트는 다양한 유형의 구현 문제점을 찾습니다. 예를 들어 모든 연결 화살표를 순회하여 테스트는 구현에 누락된 화살표가 있는지 확인합니다. 실패 경로 다음에 성공적으로 완료해야 하는 경로가 있는 이벤트 시퀀스를 사용하여 테스트는 오류 처리 코드가 나중 계산에 영향을 줄 수 있는 부분 결과를 정리하는 데 실패했는지 여부를 확인합니다. (테스트 상태 차트에 대한 자세한 정보는 중간 산출물 가이드라인: 상태 차트 및 활동 다이어그램의 테스트 아이디어를 참조하십시오.)

최종 예제는 디자인 모델의 파트를 사용합니다. 채권자와 송장 간 연관이 있으며, 여기서 지정된 채권자는 둘 이상의 미결 송장을 가질 수 있습니다.

Creditor 및 Invoice 클래스 연관 다이어그램

그림 4: Creditor 및 Invoice 클래스 간 연관

이 모델에 기반한 테스트는 채권자에게 송장이 없는 경우, 하나가 있는 경우 및 많은 송장이 있는 경우에 대해 시스템을 실행합니다. 테스터는 송장을 둘 이상의 채권자와 연관시켜야 하거나 송장에 채권자가 없는 경우가 있는지 여부를 질문합니다. (예를 들어 현재 종이 기반 시스템에서 보류 중인 작업은 채권자 없는 송장으로 추적됩니다.) 이와 같은 경우 이 문제는 분석에서 발견해야 하는 또 다른 문제가 됩니다.

누가 테스트 우선 디자인을 수행합니까?

테스트 우선 디자인은 디자인 작성자 또는 다른 누군가에 의해 수행될 수 있습니다. 작성자가 이 디자인을 수행하는 것이 가장 일반적입니다. 이렇게 하면 커뮤니케이션 오버헤드가 감소한다는 이점이 있습니다. 중간 산출물 디자이너 및 테스트 디자이너는 서로에게 내용을 설명할 필요가 없습니다. 또한 별도의 테스트 디자이너는 디자인을 잘 아는 데 시간이 걸리지만 원래 디자이너는 이미 알고 있습니다. 마지막으로 "압축기가 상태 X에서 실패하면 어떤 일이 발생합니까?"와 같은 대부분의 질문은 소프트웨어 중간 산출물 디자인 및 테스트 디자인 과정에서 하게 되는 자연스러운 질문이므로, 동일한 사람이 단 한 번 디자이너에게 질문하도록 하고 대답을 테스트 형식으로 기록할 수도 있습니다.

단점도 있습니다. 첫 번째 단점은 중간 산출물 디자이너는 일정 범위까지 자신의 실수를 알지 못한다는 점입니다. 테스트 디자인 프로세스에서 모르던 내용의 일부를 알게 되지만 다른 사람이 찾는 것보다는 적을 것입니다. 문제점의 양은 사람마다 크게 다르며 보통 디자이너의 경험 정도에 관련됩니다.

같은 사람이 소프트웨어 디자인 및 테스트 디자인을 모두 수행하는 방법의 또 다른 단점은 병행이 없다는 것입니다. 역할을 서로 다른 인원에게 할당하면 전체 노력은 더 많이 필요하지만 경과 시간은 감소할 것입니다. 인원들이 디자인에서 구현으로 빨리 진행하고 싶어하는 경우 테스트 디자인에 시간이 걸리면 초조해질 수 있습니다. 또한 계속 진행하기 위해 작업을 대충 수행하는 경향이 있습니다.

컴포넌트 디자인 시 모든 테스트 디자인을 완료할 수 있습니까?

아니오. 그 이유는 모든 결정이 디자인 시간에 이루어지는 것은 아니기 때문입니다. 구현 과정에서 작성되는 결정은 디자인에서 작성되는 테스트에 의해 제대로 테스트되지 않습니다. 이에 대한 가장 일반적 예제는 배열을 정렬하는 루틴입니다. 서로 다른 절충을 가지는 다양한 정렬 알고리즘이 있습니다. 빠른 정렬(Quicksort)은 대개 대형 배열에서는 삽입 정렬보다 빠르지만 소형 배열에서는 느리기도 합니다. 따라서 16개 이상의 요소가 있는 배열에 빠른 정렬을 사용하고, 그렇지 않으면 삽입 정렬을 사용하도록 정렬 알고리즘을 구현할 수 있습니다. 이 분업은 디자인 중간 산출물에 표시되지 않을 수 있습니다. 이를 중간 산출물에 표시 가능하지만 디자이너는 명시적 결정을 하는 이점이 가치가 없다고 결정할 수 있습니다. 배열 크기는 디자인에서 아무런 역할을 하지 않기 때문에 테스트 디자인은 무의식적으로 소형 배열만 사용할 수 있습니다. 즉, 빠른 정렬 코드가 전혀 테스트되지 않습니다.

다른 예제로 시퀀스 다이어그램의 이 부분을 고려하십시오. 이 예제는 StableStorelog() 메소드를 호출하는 SecurityManager를 표시합니다. 이 경우 log()는 실패를 리턴하지만, 이로 인해 SecurityManagerConnection.close()를 호출합니다.

보안 관리자 시퀀스 다이어그램 인스턴스

그림 5: SecurityManager 시퀀스 다이어그램 인스턴스

이 항목은 구현자에게 좋은 자료가 됩니다. log()가 실패할 때마다 연결이 종결되어야 합니다. 응답할 테스트의 질문은 구현자가 실제로 작업을 수행했고 모든 경우에 제대로 수행했는지 아니면 일부 경우에 제대로 수행했는지 여부입니다. 질문에 응답하려면 테스트 디자이너는 StableStore.log()에 대한 모든 호출을 찾고 각 호출 지점에 처리할 실패가 제공되었는지 확인해야 합니다.

StableStore.log()를 호출하는 모든 코드를 방금 본 경우, 이런 테스트를 실행하는 것이 이상하게 보일 수 있습니다. 이 코드가 실패를 제대로 처리하는지 여부만 확인할 수는 없습니까?

검수만으로 충분할 수 있습니다. 그러나 오류 처리 코드는 내재적으로 오류 발생이 위반한 가정에 따라 다르기 때문에 오류가 발생하기 쉽습니다. 이에 대한 가장 일반적 예제는 할당 실패를 처리하는 코드입니다. 예를 들어 다음과 같습니다.

  while (true) { // top level event loop     try { XEvent xe = getEvent(); ...                      // main body of program     } catch (OutOfMemoryError e) { emergencyRestart();     } }

이 코드는 정리를 통해 메모리를 사용 가능하게 만든 다음 이벤트 처리를 계속하여 메모리 오류로부터 복구하려고 시도합니다. 이 디자인이 허용되는 디자인이라고 가정해 보십시오. emergencyRestart는 메모리를 할당하지 않기 위해 조심해야 합니다. 문제점은 emergencyRestart가 특정 유틸리티 루틴을 호출하고, 이 루틴은 다른 유틸리티 루틴을 호출하고, 다른 유틸리티 루틴을 호출하는 방식으로 새 오브젝트를 할당하는 것입니다. 메모리가 없는 경우는 제외되므로 전체 프로그램이 실패합니다. 이러한 유형의 문제점은 검수를 통해 찾기 어렵습니다.

테스트 우선 디자인 및 RUP 단계

지금까지 가능한 빨리 많은 테스트 디자인을 수행한다고 암묵적으로 가정했습니다. 즉, 디자인 중간 산출물에서 가능한 모든 테스트를 도출한 후 구현 내부에 기반한 테스트만 추가합니다. 반복 목표에 전체 테스트를 맞출 수 있는 것이 아니므로 정제(Elaboration) 단계에서는 이 방법이 적절하지 않을 수 있습니다.

이해 당사자(stakeholder)에게 제품 가능성을 예시하는 아키텍처 프로토타입을 빌드하고 있다고 가정해 보십시오. 이 프로토타입은 몇 가지 핵심 유스 케이스 인스턴스에 기반하고 있을 수 있습니다. 코드를 테스트하여 인스턴스를 지원하는지 확인해야 합니다. 그러나 추가 테스트를 작성하면 손해가 있습니까? 예를 들어 프로토타입이 중요 오류 사례를 무시하는 것이 명백할 수 있습니다. 프로토타임을 실행할 테스트 케이스를 작성하여 해당 오류 처리의 필요성을 문서화하면 어떻겠습니까?

그러나 프로토타입이 작업을 수행하고 아키텍처 접근 방식이 작동하지 않으면 어떻겠습니까? 그러면 오류 처리를 위한 모든 해당 테스트와 함께 아키텍처를 사용하지 않게 됩니다. 이 경우 테스트를 디자인하는 노력은 아무런 의미가 없게 됩니다. 기다렸다가 이 개념 검증 프로토타입이 실제로 개념을 증명하는지 여부를 확인하는 데 필요한 테스트만 디자인하는 것이 좋을 수 있습니다.

이 아이디어가 사소한 것 같지만 실제 작업에는 심리적으로 큰 영향을 미칠 수 있습니다. 정제(Elaboration) 단계는 주요 위험성을 해결하는 작업입니다. 전체 프로젝트 팀은 이런 위험성에 초점을 맞춰야 합니다. 인원들이 사소한 문제에 집중하게 하면 팀의 집중력 및 에너지가 소모됩니다.

그러면 정제 단계에서 테스트 우선 디자인을 성공적으로 사용할 수 있는 부분은 어디입니까? 아키텍처 위험성을 적절하게 탐색하는 데 이 질문이 중요한 역할을 할 수 있습니다. 위험성이 실현되었는지 아니면 방지되었는지 여부를 팀이 얼마나 자세히 알고 있는지를 고려하면 디자인 프로세스가 보다 명확해지고 처음 빌드되는 아키텍처를 훨씬 향상시킬 수 있습니다.

구현/구축(Construction) 단계 동안 디자인 중간 산출물은 최종 양식에 삽입됩니다. 모든 필수 유스 케이스 실현(realization)은 모든 클래스의 인터페이스처럼 구현됩니다. 단계별 목표가 완전성이므로 전체 테스트 우선 디자인이 적절합니다. 이후 이벤트가 몇몇 테스트(있는 경우)를 무효화해야 합니다.

구현/구축(Construction) 및 전이(Transition) 단계는 일반적으로 테스트가 적절한 디자인 활동에 보다 덜 집중합니다. 이 경우 테스트 우선 디자인이 적용 가능합니다. 예를 들어 도입/인식(Inception) 단계에서 개념 검증 작업 대상에 테스트 우선 디자인을 사용할 수 있습니다. 구현/구축 및 정제(Elaboration) 단계 테스트처럼 반복 목표에 따라 테스트를 맞춰야 합니다.