26. Testing

26.1. What we want

프로그램의 검증은 중요하다.

26.1.1. Caveat

테스팅은 큰 토픽이다.

26.2. Proofs

테스트 이전에 알고리즘의 동작을 증명할 수는 없을까? 그것만으론 충분치 않다.

26.3. Testing

테스팅에는 단위 테스팅과 시스템 테스팅이 있다. 단위 테스팅은 특정 단위에 대해 테스팅하는 것이고 시스템 테스팅은 단위간 상호 작용에 대해 테스팅하는 것이다. 회귀 테스팅은 코드를 수정한 뒤 에러가 새로 생기진 않았나 다시 테스팅을 하는 것이다.

26.3.1. Regression tests

버그 리포트를 중시하자. 버그 리포트를 만들 때는 최소의 재현 가능한 예제를 첨부하자.

26.3.2. Unit tests

테스팅은 두 종류의 오류를 테스트해야 한다.

  • 가능한 범주의 실수들
  • 최악의 실수들

26.3.2.1. Testing strategy

다음 경우에 대해 테스팅을 수행해 보자.

  • 무한 루프
  • 크래시
  • 반환값이 틀림
  • 나쁜 입력

이들에 대한 테스트 케이스는 어떻게 되어야 할까?

  • 엣지 케이스
  • 경계 조건

26.3.2.2. A simple test harness

이런 식으로 이진 탐색을 체계적으로 테스트할 수 있다.

struct Test {
          std::string label;
          int val;
          std::vector<int> seq;
          bool res;
};

std::istream& operator>>(std::istream& is, Test& t);     // use the described format

int test_all(std::istream& is)
{
          int error_count = 0;
          for (Test t; is>>t; ) {
                    bool r = std::binary_search(t.seq.begin(), t.seq.end(), t.val);
                    if (r !=t.res) {
                              std::cout << "failure: test " << t.label
                                        << " binary_search: "
                                        << t.seq.size() << " elements, val==" << t.val
                                        << " –> " << t.res << '\n';
                              ++error_count;
                    }
          }
          return error_count;
}
int main()
{
          int errors = test_all(std::ifstream("my_tests.txt"));
          cout << "number of errors: " << errors << "\n";
}

26.3.2.3. Random sequences

이런 식으로 무작위 테스트 케이스를 만들 수 있다.

void make_test(const std::string& lab, int n, int base, int spread)
          // write a test description with the label lab to cout
          // generate a sequence of n elements starting at base
          // the average distance between elements is uniformly distributed
          // in [0:spread)
{
          std::cout << "{ " << lab << " " << n << " { ";
          std::vector<int> v;
          int elem = base;
          for (int i = 0; i<n; ++i) {    // make elements
                    elem+= randint(spread);
                    v.push_back(elem);
          }

          int val = base+ randint(elem–base);        // make search value
          bool found = false;
              for (int i = 0; i<n; ++i) {      // print elements and see if val is found
                        if (v[i]==val) found = true;
                        cout << v[i] << " ";
          }
          cout << "} " << found << " }\n";
}

26.3.3. Algorithms and non-algorithms

사전조건과 사후조건이 명시되어 있다면 좋겠지만 꼭 그렇지는 않다. 그러면 버그의 주된 온상은 어디일까?

26.3.3.1. Dependencies

이런 코드는 의존성을 전부 체크해야 한다.

int do_dependent(int a, int& b)            // messy function
          // undisciplined dependencies
{
          int val ;
          std::cin>>val;
          vec[val] += 10;
          std::cout << a;
          b++;
          return b;
}

26.3.3.2. Resource management

이런 코드는 자원 관리를 전부 체크해야 한다.

void do_resources1(int a, int b, const char* s) // messy function
          // undisciplined resource use
{
          FILE* f = fopen(s,"r");                // open file (C style)
          int* p = new int[a];                     // allocate some memory
          if (b<=0) throw Bad_arg();         // maybe throw an exception
          int* q = new int[b];                    // allocate some more memory
          delete[] p;                                   // deallocate the memory pointed to by p
}

이는 다음과 같이 개선될 수 있다.

void do_resources2(int a, int b, const char* s)      // less messy function
{
          std::ifstream is(s);                          // open file
          std::vector<int>v1(a);                   // create vector (owning memory)
          if (b<=0) throw Bad_arg();    // maybe throw an exception
          std::vector<int> v2(b);                  // create another vector (owning memory)
}

그래서 스코프 기반 자원 관리가 중요한 것이다.

26.3.3.3. Loops

루프나 읽기는 항상 버퍼 오버플로우에 주의해야 한다. gets() 등의 함수는 이 때문에 표준에서 없어졌다.

26.3.3.4. Branching

if-else, switch문에서는 모든 경우가 고려되었는지를 생각해야 한다.

26.3.4. System tests

테스팅은 체계적으로, 빠짐없이 이루어져야 한다.

26.3.5. Finding assumptions that do not hold

이진 탐색에서 시퀀스가 정렬되었는지에 대한 테스팅은 테스팅 모드일 때만 수행해야 한다. 선형 시간이므로 이진 탐색의 로그 시간보다 비싸기 때문이다. 반복자가 무작위 접근 반복자임을 테스트하는 것도 그렇다.

26.4. Design for Testing

  • 잘 정의된 인터페이스를 사용하라.
  • 연산을 텍스트화할 수 있도록 하라.
  • 시스템 테스팅 이전에 사전조건을 테스트화하라.
  • 의존성을 최소화하고, 의존성이 존재한다면 명시하라.
  • 자원 관리 방식을 명확히 하라.

26.5. Debugging

좋은 테스팅은 디버깅을 줄인다.

26.6. Performance

나쁜 알고리즘 하에서는 절대 좋은 성능이 나올 수 없다. 하지만 나쁜 알고리즘만이 나쁜 성능을 내는 것은 아니다.

  • 정보의 반복된 재계산
  • 같은 사실에 대한 반복된 체크
  • 디스크에 대한 반복된 접근

26.6.1. Timing

std::chrono::system_clock으로 벤치마크를 하자.

26.7. References

더 많은 정보를 원한다면: https://en.cppreference.com/w/cpp/chrono