가이드라인: 메소드 호출에 대한 테스트 아이디어
테스트 아이디어는 그럴듯한 소프트웨어 결함과 해당 결함을 가장 잘 발견할 수 있는 방법을 기반으로 합니다. 이 가이드라인은 코드가 메소드 호출의 결과를 처리하지 않는 사례를 발견하는 메소드를 설명합니다.
관계
기본 설명

소개

다음은 결함이 있는 코드 예제입니다.

File file = new File(stringName);
file.delete();

결함은 File.delete가 실패할 수 있으나 코드가 이것을 확인하지 않는다는 것입니다. 수정하려면 여기에 기울임꼴로 표시된 코드가 추가로 필요합니다.

File file = new File(stringName);



if (file.delete()


== false) {...}

이 가이드라인은 코드가 메소드 호출의 결과를 처리하지 않는 사례를 발견하는 메소드를 설명합니다. (입력하는 내용이 무엇이든 올바른 결과를 생성하는 메소드를 호출한 것으로 가정합니다. 이것은 테스트해야 할 일이고 호출된 메소드에 대한 테스트 아이디어를 작성하는 것은 별개의 타스크입니다. 즉, File.delete를 테스트하는 것은 사용자의 작업이 아닙니다.)

주요 개념은 메소드 호출에 대한 별개의 비처리 관련 결과에 대한 테스트 아이디어를 작성해야 한다는 것입니다. 용어를 정의하려면 먼저 결과를 보십시오. 메소드가 실행될 때 상태가 변경됩니다. 다음은 몇 가지 예제입니다.

  • 런타임 스택에서 리턴값을 푸시(push)합니다.
  • 예외를 처리합니다.
  • 글로벌 변수를 변경합니다.
  • 데이터베이스에서 레코드를 갱신합니다.
  • 네트워크를 통해 데이터를 보냅니다.
  • 표준 출력으로 메시지를 인쇄합니다.

일부 예제를 사용하여 관계를 다시 보십시오.

  • 표준 출력으로 메시지를 인쇄하도록 메소드가 호출되었다고 가정해 보십시오. 이것은 "상태 변경"이지만 이 프로그램의 후속 처리에 영향을 미치지 못합니다. 인쇄 여부에 관계없이, 심지어 아무 것도 인쇄되지 않아도 코드 실행에 영향을 미칠 수 없습니다.
  • 성공인 경우 true를 리턴하고 실패인 경우 false를 리턴하면 프로그램은 결과를 기반으로 분기합니다. 그러므로 해당 리턴값은 관계형입니다.
  • 호출된 메소드가 코드가 나중에 읽고 사용하는 데이터베이스 레코드를 갱신하면 레코드를 갱신한 결과는 관계형입니다.

(적절한 것과 적절하지 않은 것 사이에 절대적인 구분은 없습니다. 인쇄를 호출하여 메소드는 할당해야 할 버퍼의 원인이 되고 이러한 할당은 적절합니다. 결함이 어떤 버퍼가 할당되느냐에 따라 발생하는 것은 있을 수 있는 일입니다. 이는 있을 수는 있지만 모두 적합한 것은 아닙니다.)

종종 메소드에는 아주 많은 결과가 있지만 그 중 일부만 구별됩니다. 예를 들어, 디스크에 바이트를 쓰는 메소드를 고려하십시오. 0보다 작은 숫자를 리턴하여 실패를 표시할 수 있습니다. 그렇지 않으면 기록된 바이트 수를 리턴합니다(요청한 수보다 작을 수 있음). 많은 수의 가능성이 세 가지 별개의 결과 그룹으로 묶일 수 있습니다.

  • 0보다 작은 숫자
  • 요청한 수와 기록된 수가 같음
  • 일부 바이트가 기록되었으나 요청한 수보다 작음

0보다 작은 모든 값을 구분할 수 있는 적절한 프로그램이 없기 때문에 하나의 결과에 그룹으로 묶입니다. 이 모두는 오류로 처리되어야 합니다. 비슷한 경우로 코드가 500바이트를 기록하도록 요청한 경우, 실제로 34가 기록되거나 340이 기록되거나에 관계없이 양쪽 모두 기록되지 않은 바이트와 마찬가지로 처리됩니다. (0과 같은 일부 값에 대해 다른 작업이 수행되어야 할 경우 새로운 별개의 결과가 형성됩니다.)

용어 정의 시 설명해야 하는 마지막 단어가 있습니다. 이러한 특수 테스트 기법은 이미 처리된 별개의 결과와는 관계가 없습니다. 다시 이 코드를 고려하십시오.

File file = new File(stringName);
if (file.delete() == false) {...}

두 개의 별개의 결과(true 및 false)가 있습니다. 코드에서 이를 처리합니다. 이는 올바르지 않게 처리할 수 있지만, 중간 산출물 가이드라인: 부울 및 경계에 대한 테스트 아이디어의 테스트 아이디어가 이를 확인합니다. 이러한 테스트 기법은 별개의 코드가 특정하게 처리하지 않는 별개의 결과와 관계가 있습니다. 이는 구별이 관련 없다고 여겼거나 단순히 못보고 지나치는 두 가지 이유 때문에 발생합니다. 첫 번째 경우의 예제는 다음과 같습니다.

result = m.method();
switch (result) {
    case FAIL:
    case CRASH:
       ...
       break;
    case DEFER:
       ...
       break;
    default:
       ...
       break;
}

FAIL CRASH는 동일한 코드로 처리됩니다. 정말로 알맞은지 확인하는 것이 좋습니다. 못보고 지나친 경우의 예제는 다음과 같습니다.

result = s.shutdown();
if (result == PANIC) {
   ...
} else {
   // success! Shut down the reactor.
   ...
} 

시스템 종료로 추가의 별개 결과가 리턴될 수 있습니다: RETRY. 작성된 코드는 이 경우를 성공한 경우와 동일하게 다루는데, 이는 분명히 틀린 것입니다.

테스트 아이디어 찾기

그러므로 목적은 이전에 못보고 지나쳤던 별개의 관계이 있는 결과를 고려하는 것입니다. 이는 불가능해 보입니다. 이전에 알아차리지 못했는데 왜 지금 관계가 있음을 깨달을 수 있겠습니까?

프로그래밍 프레임적 사고가 아니라 테스트 프레임적 사고에서 코드를 조직적으로 재조사하면 때때로 새로운 사고가 가능하다는 것이 그 해답입니다. 코드를 꼼꼼하게 확인하거나, 호출한 메소드를 검토하거나, 문서를 다시 확인하면서 스스로의 가정에 의문을 제기할 수 있습니다. 몇 가지 살펴볼 경우는 다음과 같습니다.

"불가능한" 경우

종종 오류 리턴이 불가능해 보일 수 있습니다. 가정을 다시 한 번 확인하십시오.

이 예제는 임시 파일을 처리하는 공통 Unix 구문의 Java 구현을 표시합니다.

File file = new File("tempfile");
FileOutputStream s;
try {
    // open the temp file.
    s = new FileOutputStream(file);
} catch (IOException e) {...}
// Make sure temp file will be deleted
file.delete();

목적은 프로그램 종료 방법에 관계없이 임시 파일이 항상 삭제되도록 확인하는 것입니다. 임시 파일을 작성하고 바로 삭제하여 이를 수행할 수 있습니다. Unix에서는 삭제된 파일로 작업을 계속할 수 있으며, 프로세스를 종료하면 운영 체제가 정리 작업을 담당합니다. 세심하지 않은 Unix 프로그래머의 경우 실패한 삭제를 확인하는 코드를 작성하지 않을 수 있습니다. 성공적으로 파일을 작성했기 때문에 삭제할 수도 있게 됩니다.

이러한 편법은 Windows에서는 동작하지 않습니다. 파일은 열려 있기 때문에 삭제할 수 없습니다. 이를 발견하기는 어렵습니다. 2000년 8월, Java 문서는 삭제에 실패할 수 있는 상황을 열거하지는 않고 단순하게 실패가 가능하다고만 했습니다. 그러나 "테스트 모드"에서 프로그래머는 자신이 가정한 내용에 의문을 가질 수도 있습니다. 코드가 "한 번 써서 어디서든지 실행되어야" 하기 때문에, Windows에서 File.delete가 실패했을 때 Windows 프로그래머에게 질문하여 이 사실을 발견할 수 있습니다.

"관련이 없는" 경우

별개의 관련 값을 알지 못하게 하는 다른 요소는 상관 없음이 이미 증명되었습니다. Java Comparatorcompare 메소드는 숫자 <0, 0 또는 숫자 >0을 리턴합니다. 이것이 시도할 세 가지 별개의 경우입니다. 이 코드는 이 중 둘을 묶어서 취급합니다.

void allCheck(Comparator c) {
   ...
   if (c.compare(o1, o2) <= 0) {
      ...
   } else {
      ...
   } 

그러나 이는 잘못되었을 수 있습니다. 둘 사이에 차이점이 정말 없을 거라고 믿어도 두 경우를 분리해서 시도해야 차이 여부를 밝혀낼 수 있습니다. (믿음이야말로 테스트 대상입니다.) 다른 이유로 if 구문의 then을 한 번 이상 실행할 수도 있음에 유의하십시오. 그 중 하나를 0 미만의 결과로 시도하고 나머지 하나는 정확하게 0과 일치하는 결과로 시도해 보십시오.

미발견 예외

예외는 별개의 결과의 한 종류입니다. 다음 코드를 가정해 보십시오.

void process(Reader r) {
   ...
   try {
      ...
      int c = r.read();
      ...
   } catch (IOException e) {
      ...
   }
}

핸들러 코드가 읽기 실패에 적당한지 여부를 확인하려고 합니다. 그러나 예외가 명확하게 처리되지 않았다고 가정해 보십시오. 대신에 테스트 중 코드를 통해 위쪽으로 전파하는 것은 허용됩니다. Java에서는 다음과 같을 수 있습니다.

void process(Reader r) 


throws IOException {
    ...
    int c = r.read();
    ...
}

이러한 기법은 코드가 명확하게 처리하지 않는다 하더라도 해당 경우를 테스트하도록 요청합니다. 이유는 무엇입니까? 다음 종류의 결함 때문입니다.

void process(Reader r) throws IOException {
    ...
    


Tracker.hold(this);
    ...
    int c = r.read();
    ...
    


Tracker.release(this);
    ...
}

여기에서 코드는 Tracker.hold를 통해 글로벌 상태에 영향을 줍니다. 예외가 발생하면 Tracker.release가 호출되지 않습니다.

(릴리스에 대한 실패는 분명하고 즉각적인 결과로 나타나지 않습니다. 문제점은 프로세스가 다시 호출될 때까지 볼 수 없습니다. 두 번째 오브젝트 보류 시도가 실패하게 됩니다. 이러한 결함에 대한 정보는 Keith Stobie의 "Testing for Exceptions"를 참조하십시오. ( Adobe Reader 다운로드))

발견되지 않은 결함

이러한 특정 기법은 메소드 호출과 관련된 모든 결함을 다루지는 않습니다. 다음과 같이 발견하기 힘든 두 가지 유형이 있습니다.

올바르지 않은 인수

다음과 같은 두 행의 C 코드를 고려하십시오(첫 번째 행은 잘못되었고 두 번째 행은 올바름).

... strncmp(s1, s2, strlen(s1)) ...
... strncmp(s1, s2, strlen(


s2)) ...

strncmp는 두 문자열을 비교하고 첫 번째가 두 번째(디렉토리에서 앞에 위치)보다 작을 경우 0 미만의 숫자를 리턴합니다. 둘이 같을 경우 "0"을 리턴합니다. 첫 번째가 더 클 경우 0보다 큰 숫자를 리턴합니다. 그러나 이것은 세 번째 인수가 제공한 문자의 수만 비교합니다. 문제점은 첫 번째 문자열의 길이는 비교를 제한하는 데 사용되는 반면, 이는 두 번째의 길이가 되어야 한다는 것입니다.

이 기법은 각각의 별개의 리턴값에 하나씩 세 가지 테스트를 필요로 합니다. 사용할 수 있는 세 가지는 다음과 같습니다.

s1 s2 예상 결과 실제 결과
"a" "bbb" <0 <0
"bbb" "a" >0 >0
"foo" "foo" =0 =0

이 기법에서는 세 번째 인수가 특정 값을 가지도록 강제 실행하지 않기 때문에 결함이 발견되지 않습니다. 필요한 것은 다음과 같은 테스트 케이스입니다.

s1 s2 예상 결과 실제 결과
"foo" "food" <0 =0

이런 결함을 발견하는 데 적합한 기법이 있지만 실제로는 거의 사용되지 않습니다. 테스트 노력은 많은 유형의 결함을 대상으로 하는 다양한 테스트 세트에 투자하는 것이 좋습니다(부가적으로 이러한 유형의 결함을 발견할 수도 있음).

뚜렷하지 않은 결과

메소드 단위로 코드를 작성하고 테스트할 때 위험이 있습니다. 예제는 다음과 같습니다. 두 가지 메소드가 있습니다. 첫 번째는 connect로 네트워크 연결을 구성하려 합니다.

void connect() {
   ...
   Integer portNumber = serverPortFromUser();
   if (portNumber == null) {
      // pop up message about invalid port number
      return;
   }

포트 번호가 필요할 때 serverPortFromUser를 호출합니다. 이 메소드는 두 개의 별개의 값을 리턴합니다. 선택한 숫자가 1000 이상의 올바른 숫자이면 사용자가 선택한 포트 번호를 리턴합니다. 그렇지 않으면 널(null)을 리턴합니다. 널(null)이 리턴되면 테스트 중인 코드는 오류 메시지를 팝업으로 표시하고 종료됩니다.

connect를 테스트할 때 다음과 같이 의도한 대로 동작합니다. 올바른 포트 번호를 사용한 경우 연결이 구성되고 올바르지 않은 경우 팝업 메시지를 표시합니다.

serverPortFromUser로의 코드는 조금 더 복잡합니다. 처음에는 문자열을 요청하고 표준 확인 및 취소 단추를 가진 창을 표시합니다. 사용자가 취하는 동작을 기반으로 네 가지 경우가 있습니다.

  1. 사용자가 올바른 숫자를 입력하면 그 숫자가 리턴됩니다.
  2. 숫자가 1000 미만으로 너무 작으면 널(null)이 리턴되면서 올바르지 않은 포트 번호에 대한 메시지가 표시됩니다.
  3. 번호 형식이 잘못되면 널(null)이 다시 리턴되고 동일한 메시지가 표시됩니다.
  4. 사용자가 취소를 클릭하면 널(null)이 리턴됩니다.

이 코드 역시 의도한 대로 동작합니다.

두 코드 모음의 조합은 잘못된 결과를 가져옵니다. 사용자가 취소를 누르면 올바르지 않은 포트 번호에 대한 메시지가 표시됩니다. 모든 코드는 의도한 대로 동작하지만 전체적인 영향은 계속 잘못됩니다. 올바른 방법으로 테스트했지만 결함을 발견하지 못했습니다.

여기서의 문제점은 널(null)이 두 개의 별개의 의미("잘못된 값"과 "사용자 취소")를 나타내는 하나의 결과라는 것입니다. 이러한 기법에서는 serverPortFromUser 디자인으로 인한 문제점을 사용자에게 알리도록 강제 실행하지 않습니다.

테스트가 도움이 될 수 있습니다. 각각의 네 가지 경우에서 의도된 값이 리턴되는 것을 확인하기 위해 serverPortFromUser를 분리하여 테스트할 경우 사용 컨텍스트가 손실됩니다. 대신에 connect를 사용하여 테스트했다고 가정해 보십시오. 두 메소드를 동시에 연습할 수 있는 네 가지 테스트가 있습니다.

입력 예상 결과 사고 프로세스
사용자가 "1000"을 입력 1000 포트에 대한 연결이 열림 serverPortFromUser는 사용된 숫자를 리턴

사용자가 "999"를 입력

올바르지 않은 포트 번호에 대한 팝업이 표시됨

serverPortFromUser는 팝업을 표시하면서 널(null)을 리턴

사용자가 "i99"를 입력

올바르지 않은 포트 번호에 대한 팝업이 표시됨 serverPortFromUser는 팝업을 표시하면서 널(null)을 리턴
사용자가 취소를 클릭 전체 연결 프로세스가 취소됨 serverPortFromUser는 널(null)을 리턴하지만 이것은 바람직한 방법이 아님

종종 대규모 컨텍스트의 테스트는 소규모 테스트를 벗어나는 통합 문제를 드러냅니다. 그리고 종종 테스트 디자인 중 신중하게 사고하면 테스트 실행 전에 문제점을 발견하게 됩니다. (그러나 여기에서 결함이 발견되지 않으면 테스트 실행 중 발견됩니다.)