중요

번역은 여러분이 참여할 수 있는 커뮤니티 활동입니다. 이 페이지는 현재 100.00% 번역되었습니다.

6. 단위 테스트 작업

2007년 11월부터, 마스터로 들어가는 모든 새 기능들은 단위 테스트를 포함해야 합니다. 처음에는 이 요구 사항을 qgis_core 에만 적용했지만, 사람들이 다음 절들에서 설명하는 단위 테스트 작업에 익숙해지면 코드 베이스의 다른 부분들로 이 요구 사항을 확장시켜 나갈 것입니다.

6.1. QGIS 테스트 작업 프레임워크 — 개요

단위 테스트 작업은 QTestLib(Qt 테스트 작업 라이브러리)과 CTest(CMake 빌드 절차의 일환으로써 테스트를 컴파일하고 실행하기 위한 프레임워크)의 조합을 사용해서 수행됩니다. 자세한 내용을 설명하기 전에 이 절차의 개요를 살펴봅시다:

  1. 여러분이 테스트해보고 싶은 코드가, 예를 들면 클래스 또는 함수가 있을 수 있습니다. 극단적인 프로그래밍 옹호자들은 여러분이 테스트를 빌드하기 시작할 때는 아직 코드를 작성도 하지 않은 상태여야 하며, 코드를 구현하면서 테스트에 각각의 새 기능 부분을 추가하는 즉시 무결성을 검증할 수 있어야 한다고 제안합니다. 실제로는 대부분의 응용 프로그램 로직이 이미 구현된 후 테스트 작업 프레임워크를 사용해서 시작하기 때문에, QGIS에 있는 기존 코드에 대해 테스트를 작성해야 할 수도 있습니다.

  2. 단위 테스트를 생성하십시오. 핵심 라이브러리인 경우 <QGIS Source Dir>/tests/src/core 에 생성합니다. 이 테스트는 기본적으로 클래스의 인스턴스를 생성한 다음 해당 클래스에 대해 몇몇 메소드를 호출하는 클라이언트입니다. 이 테스트는 각 메소드가 반환하는 값이 예상 값과 일치하는지 확인하기 위해 반환 값을 점검할 것입니다. 호출한 메소드 가운데 하나라도 통과하지 못하는 경우, 단위 테스트가 실패한 것입니다.

  3. 여러분의 테스트 클래스에 QtTestLib 매크로를 포함시키시오. Qt MOC(Meta Object Compiler)이 이 매크로를 처리해서 테스트 클래스를 실행 가능한 응용 프로그램으로 확장합니다.

  4. 여러분의 테스트 디렉터리에 있는 CMakeLists.txt 파일에 테스트를 빌드할 부분(section)을 추가하십시오.

  5. CCMake/CMakeSetup에서 ENABLE_TESTING 이 활성화되었는지 확인하십시오. 여러분이 make 를 입력할 때 여러분의 테스트가 실제로 컴파일되도록 해줄 것입니다.

  6. 여러분의 테스트가 데이터 기반인 경우 (예를 들면 셰이프파일을 불러와야 하는 경우) <QGIS Source Dir>/tests/testdata 에 테스트 데이터를 선택적으로 추가할 수 있습니다. 이런 테스트 데이터의 용량은 가능한 한 작아야 하며 가능한 경우 언제나 이미 존재하는 기존 데이터셋을 사용해야 합니다. 테스트는 절대 제자리에서(in situ) 이 데이터를 수정해서는 안 되며, 필요한 경우 어딘가에 임시 복사본을 만들어야 합니다.

  7. 여러분의 소스를 컴파일한 다음 설치하십시오. 일반적인 make && (sudo) make install 절차를 사용하십시오.

  8. 여러분의 테스트를 실행하십시오. 일반적으로 make install 단계 다음에 make test 명령어를 실행하면 되지만, 테스트 실행에 대해 더 세밀한 제어를 할 수 있는 다른 접근법들도 설명할 것입니다.

이러한 개요를 기억해두고서 좀 더 자세히 살펴보겠습니다. 이미 CMake 및 소스 트리에 있는 다른 위치들에서 대부분의 환경설정을 끝마쳤기 때문에, 여러분이 해야 할 일은 쉬운 부분밖에 남지 않았습니다 — 단위 테스트를 작성하는 것이죠!

6.2. 단위 테스트 생성하기

단위 테스트를 생성하는 일은 쉽습니다 — 일반적으로 .cpp 파일 하나를 생성하고 (.h 파일은 쓰지 않습니다) 모든 테스트 메소드들을 보이드(void)를 반환하는 비공개 메소드로 구현하면 됩니다. 다음 부분에서 QgsRasterLayer 에 대한 단순 테스트 클래스를 사용해서 설명할 것입니다. 관습에 따라 테스트의 이름은 테스트 대상인 클래스의 이름 앞에 ‘Test’ 접두어를 붙인 이름이 될 것입니다. 따라서 testqgsrasterlayer.cpp 라는 파일에 테스트를 구현하면 클래스 자체는 TestQgsRasterLayer 가 될 것입니다. 먼저 표준 저작권 배너를 추가하십시오:

/***************************************************************************
 testqgsvectorfilewriter.cpp
 --------------------------------------
  Date : Friday, Jan 27, 2015
  Copyright: (C) 2015 by Tim Sutton
  Email: [email protected]
 ***************************************************************************
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 ***************************************************************************/

그 다음 실행하고자 하는 테스트에 필요한 include 들을 입력하십시오. 모든 테스트가 가지고 있어야 할 특별한 include 가 하나 있습니다:

#include <QtTest/QtTest>

그 후에는 클래스를 평소대로 계속 구현하고, 필요한 헤더를 가져와서 사용하면 됩니다:

//Qt includes...
#include <QObject>
#include <QString>
#include <QObject>
#include <QApplication>
#include <QFileInfo>
#include <QDir>

//qgis includes...
#include <qgsrasterlayer.h>
#include <qgsrasterbandstats.h>
#include <qgsapplication.h>

단일 파일 안에 클래스 선언과 구현을 결합하고 있기 때문에 이 다음은 클래스를 선언해야 합니다. 독시젠(Doxygen) 문서로 시작합시다. 테스트 사례는 모두 제대로 문서화되어야 합니다. 생성된 독시젠 문서에서 모든 UnitTests 가 하나의 모듈로 나타나도록 독시젠 ingroup 명령어를 사용합니다. 그 다음엔 단위 테스트에 대한 짧은 설명이 오며, 클래스는 반드시 QObject 로부터 상속받아야만 하고 Q_OBJECT 매크로를 포함해야만 합니다.

/** \ingroup UnitTests
 * This is a unit test for the QgsRasterLayer class.
 */

class TestQgsRasterLayer: public QObject
{
    Q_OBJECT

테스트 메소드들은 모두 비공개 슬롯으로 구현됩니다. QtTest 프레임워크는 테스트 클래스에 있는 비공개 슬롯 메소드들을 각각 순차적으로 호출할 것입니다. 구현된 경우 단위 테스트 시작 시(initTestCase), 단위 테스트 종료 시(cleanupTestCase) 호출될 ‘특수’ 메소드들이 4개 있습니다. 각 테스트 메소드를 호출하기 전에 init() 메소드를 호출할 것이고 각 테스트 메소드를 호출한 후에 cleanup() 메소드를 호출할 것입니다. 이 메소드들은 각각의 테스트와 테스트 단위 전체를 실행하기 전에 리소스를 할당하고 정리할 수 있게 해준다는 점에서 편리합니다.

private slots:
  // will be called before the first testfunction is executed.
  void initTestCase();
  // will be called after the last testfunction was executed.
  void cleanupTestCase(){};
  // will be called before each testfunction is executed.
  void init(){};
  // will be called after every testfunction.
  void cleanup();

이 다음에 여러분의 테스트 메소드들이 옵니다. 모든 메소드는 어떤 파라미터도 받지 말아야 하며 보이드를 반환해야 합니다. 이 메소드들은 선언 순서대로 호출될 것입니다. 여기에서는 테스트 작업의 두 가지 유형을 보여주는 메소드 2개를 구현합니다.

첫 번째로 클래스의 여러 부분들이 제대로 동작하는지 전체적으로 테스트하려 합니다. 기능적인 테스트 작업 접근법을 사용하면 됩니다. 다시, 극단적인 프로그래머들은 클래스를 구현하기 전에 이런 테스트들을 작성하라고 제안할 것입니다. 그 다음 클래스를 구현해나가면서 단위 테스트를 반복 실행하십시오. 여러분의 클래스 구현 작업이 진행될수록 더 많은 테스트 함수들이 성공적으로 종료될 것입니다. 그리고 전체 단위 테스트를 통과했을 때, 여러분의 새 클래스도 구현이 완료되고 그 무결성을 반복 가능한 방법으로 검증할 수 있습니다.

단위 테스트는 일반적으로 클래스의 공개 API만 테스트할 것이고, 접근자(accessor) 및 변이유발자(mutator)에 대한 테스트를 작성할 필요는 없습니다. 접근자 또는 변이유발자가 예상한대로 작동하지 않는 일이 일어날 경우, 이를 확인하기 위한 회귀 테스트 를 구현하십시오.

//
// Functional Testing
//

/** Check if a raster is valid. */
void isValid();

// more functional tests here ...

6.2.1. 회귀 테스트 구현하기

다음으로 회귀 테스트(regression test)를 구현합니다. 회귀 테스트는 다음 예시처럼 특정 버그의 조건을 복제하도록 구현해야 합니다:

  1. 래스터의 셀 개수가 1씩 틀려서 래스터 밴드에 대한 모든 통계가 엉망이 된다는 보고서를 이메일로 받았습니다.

  2. 버그 보고서를 열었습니다. (티켓 #832)

  3. 버그를 복제하는 회귀 테스트를 소용량 데이터셋(10x10 래스터)를 사용해서 생성했습니다.

  4. 테스트를 실행했고, 실제로 셀 개수가 틀린다는 사실을 (100개가 아니라 99개로 나온다는 사실을) 검증했습니다.

  5. 그 다음 버그를 수정했고, 단위 테스트와 회귀 테스트를 다시 실행해서 통과했습니다. 우리는 버그 픽스에 회귀 테스트를 포함시켜 커밋했습니다. 이제 향후 어떤 사람이 소스 코드에서 이를 다시 위반하는 경우, 코드가 과거로 되돌아갔다는 사실을 바로 확인할 수 있습니다.

    더 나은 점은, 향후 어떤 변경 사항이라도 커밋하기 전에 이 테스트들을 실행하면 변경 사항이 예상하지 못한 부작용을 발생시키지 않는지 — 이를테면 기존 기능을 망가뜨리지 않는지 확인할 수 있다는 것입니다.

회귀 테스트의 장점이 또 하나 있습니다 — 시간을 절약할 수 있게 해줍니다. 여러분이 소스를 변경한 다음 응용 프로그램을 실행해서 문제점을 재현하기 위한 일련의 복잡한 단계들을 수행해서 버그를 수정했던 적이 있다면, 버그를 수정하기 전에 회귀 테스트를 구현하는 것만으로도 버그 해결을 위한 테스트 작업을 효율적인 방식으로 자동화할 수 있다는 사실을 즉각 알 수 있을 것입니다.

회귀 테스트를 구현하려면, 테스트 함수들에 대해 regression<TicketID> 라는 명명 관습을 준수해야 합니다. 회귀 테스트에 대한 티켓이 존재하지 않는 경우, 먼저 티켓을 생성해야 합니다. 이런 접근법을 사용하면 회귀 테스트를 실패한 사람이 자세한 정보를 쉽게 찾을 수 있습니다.

//
// Regression Testing
//

/** This is our second test case...to check if a raster
 *  reports its dimensions properly. It is a regression test
 *  for ticket #832 which was fixed with change r7650.
 */
void regression832();

// more regression tests go here ...

마지막으로 여러분의 테스트 클래스 선언에서 단위 테스트에 필요할 수도 있는 모든 데이터 멤버들과 도우미 메소드들을 비공개로 선언할 수 있습니다. 이번 예시의 경우 모든 테스트 메소드가 사용할 수 있는 QgsRasterLayer * 를 선언할 것입니다. 다른 모든 테스트보다 먼저 실행되는 initTestCase() 함수가 래스터 레이어를 생성한 다음, 모든 테스트 뒤에 실행되는 cleanupTestCase() 함수를 사용해서 삭제할 것입니다. (여러 테스트 메소드들이 호출할 수도 있는) 도우미 메소드들을 비공개로 선언하면, 테스트를 컴파일했을 때 생성되는 QTest 실행 파일이 이들을 자동 실행하지 않도록 보장할 수 있습니다.

  private:
    // Here we have any data structures that may need to
    // be used in many test cases.
    QgsRasterLayer * mpLayer;
};

이것으로 클래스 선언이 끝났습니다. 구현은 동일한 파일 다음 부분에 차례로 작성하기만 하면 됩니다. 먼저 초기화(init) 및 정리(cleanup) 함수들을 살펴보겠습니다:

void TestQgsRasterLayer::initTestCase()
{
  // init QGIS's paths - true means that all path will be inited from prefix
  QString qgisPath = QCoreApplication::applicationDirPath ();
  QgsApplication::setPrefixPath(qgisPath, TRUE);
#ifdef Q_OS_LINUX
  QgsApplication::setPkgDataPath(qgisPath + "/../share/qgis");
#endif
  //create some objects that will be used in all tests...

  std::cout << "PrefixPATH: " << QgsApplication::prefixPath().toLocal8Bit().data() << std::endl;
  std::cout << "PluginPATH: " << QgsApplication::pluginPath().toLocal8Bit().data() << std::endl;
  std::cout << "PkgData PATH: " << QgsApplication::pkgDataPath().toLocal8Bit().data() << std::endl;
  std::cout << "User DB PATH: " << QgsApplication::qgisUserDbFilePath().toLocal8Bit().data() << std::endl;

  //create a raster layer that will be used in all tests...
  QString myFileName (TEST_DATA_DIR); //defined in CmakeLists.txt
  myFileName = myFileName + QDir::separator() + "tenbytenraster.asc";
  QFileInfo myRasterFileInfo ( myFileName );
  mpLayer = new QgsRasterLayer ( myRasterFileInfo.filePath(),
  myRasterFileInfo.completeBaseName() );
}

void TestQgsRasterLayer::cleanupTestCase()
{
  delete mpLayer;
}

앞의 초기화 함수는 두 가지 흥미로운 사실을 보여줍니다.

  1. srs.db 파일 같은 리소스를 제대로 찾을 수 있도록 QGIS 응용 프로그램 데이터 경로를 직접 설정해줘야 합니다.

  2. 다음으로, 이 테스트가 데이터 기반이기 때문에 tenbytenraster.asc 파일의 위치를 일반적으로 찾을 수 있는 방법을 제공해야 합니다. 컴파일러가 정의하는 TEST_DATA_PATH 를 사용하면 됩니다. <QGIS Source Root>/tests/CMakeLists.txt 다음에 있는 CMakeLists.txt 환경설정 파일에 이 정의를 생성하는데, 모든 QGIS 단위 테스트가 이 정의를 사용할 수 있습니다. 여러분의 테스트를 위한 테스트 데이터가 필요한 경우, <QGIS Source Root>/tests/testdata 아래에 해당 데이터를 커밋하십시오. 여기엔 용량이 아주 작은 데이터셋만 커밋해야 합니다. 테스트가 테스트 데이터를 수정해야 하는 경우, 먼저 복사본을 만들어야 합니다.

Qt는 데이터 기반 테스트 작업을 위한 흥미로운 다른 메커니즘을 몇 개 제공하기 때문에, 이 주제에 대해 더 자세히 알고 싶다면 Qt 문서를 읽어보시기 바랍니다.

다음으로 기능 테스트를 살펴보겠습니다. isValid() 테스트는 initTestCase() 에 래스터 레이어를 정확히 불러왔는지만 확인합니다. QVERIFY 는 테스트 조건을 평가하는 데 사용할 수 있는 Qt 매크로입니다. Qt는 이뿐만이 아니라 다음과 같은 매크로들을 테스트에 사용할 수 있도록 제공하고 있습니다:

  • QCOMPARE ( actual, expected )

  • QEXPECT_FAIL ( dataIndex, comment, mode )

  • QFAIL ( message )

  • QFETCH ( type, name )

  • QSKIP ( description, mode )

  • QTEST ( actual, testElement )

  • QTEST_APPLESS_MAIN ( TestClass )

  • QTEST_MAIN ( TestClass )

  • QTEST_NOOP_MAIN ()

  • QVERIFY2 ( condition, message )

  • QVERIFY ( condition )

  • QWARN ( message )

이 매크로들 가운데 일부는 데이터 기반 테스트 작업에 Qt 프레임워크를 사용하는 경우에만 유용합니다. (자세한 내용은 Qt 문서를 참조하세요.)

void TestQgsRasterLayer::isValid()
{
  QVERIFY ( mpLayer->isValid() );
}

기능 테스트는 가능한 경우 일반적으로 클래스들의 공개 API 기능의 모든 범위를 포괄합니다. 기능 테스트를 완료하고 나면 회귀 테스트 예시를 살펴볼 수 있습니다.

버그 #832의 문제점이 셀 개수를 잘못 세는 것이기 때문에, QVERIFY를 사용해서 셀 개수가 예상 값과 일치하는지만 확인하도록 테스트를 작성하면 됩니다:

void TestQgsRasterLayer::regression832()
{
  QVERIFY ( mpLayer->getRasterXDim() == 10 );
  QVERIFY ( mpLayer->getRasterYDim() == 10 );
  // regression check for ticket #832
  // note getRasterBandStats call is base 1
  QVERIFY ( mpLayer->getRasterBandStats(1).elementCountInt == 100 );
}

단위 테스트 함수들을 모두 구현했다면, 테스트 클래스에 마지막으로 추가해야 할 것이 하나 있습니다:

QTEST_MAIN(TestQgsRasterLayer)
#include "testqgsrasterlayer.moc"

이 두 줄의 목적은 Qt의 MOC에 이 파일이 QtTest라는 사실을 알려주는 것입니다. 이렇게 하면 각각의 테스트 함수를 차례로 호출하는 주 메소드를 생성할 것입니다. 마지막 줄은 MOC가 생성한 소스에 대한 include 입니다. 여러분은 testqgsrasterlayer 를 소문자로 된 여러분의 클래스 이름으로 바꿔줘야 합니다.

6.3. 렌더링 테스트에서 이미지들을 비교하기

서로 다른 환경에서 이미지를 렌더링하면 플랫폼 별 구현(예를 들면 서로 다른 글꼴 렌더링이나 위신호 제거 알고리즘), 시스템 상에서 사용할 수 있는 글꼴, 그리고 기타 모호한 이유 때문에 미묘한 차이가 발생할 수 있습니다.

트래비스 상에서 렌더링 테스트를 실행해서 실패한 경우, 트래비스 로그의 최하단에 있는 대시 링크를 찾아보십시오. 이 링크는 여러분이 렌더링된 이미지와 예상 이미지를 비교해볼 수 있는 CDash 페이지를 가리킵니다. 이 페이지에서 기준 이미지와 일치하지 않는 모든 픽셀을 빨간색으로 강조한 이미지의 “차이”를 확인할 수 있습니다.

QGIS 단위 테스트 시스템은 렌더링된 이미지가 기준 이미지와 다를 수도 있는 경우를 나타내는 데 쓰이는 “마스크” 이미지들을 추가하는 기능을 지원합니다. 마스크 이미지는 (기준 이미지와 같은 이름이지만 _mask.png 접미어를 붙인) 이미지로, 기준 이미지와 동일한 크기여야 합니다. 마스크 이미지에서 픽셀 값은 개별 픽셀이 기준 이미지와 얼마나 다를 수 있는지를 나타내기 때문에, 검은색 픽셀은 렌더링된 이미지에 있는 해당 픽셀이 기준 이미지에 있는 동일 픽셀과 정확히 일치해야만 한다는 사실을 나타냅니다. 렌더링된 이미지에서 RGB(2,2,2) 값을 가진 픽셀은 기준 이미지의 동일 픽셀 RGB 값에서 2까지 변할 수 있다는 의미이며, 완전히 하얀색인 RGB(255,255,255) 값은 렌더링된 이미지와 예상 이미지를 비교할 때 해당 픽셀을 사실상 무시한다는 의미입니다.

scripts/generate_test_mask_image.py 유틸리티 스크립트를 사용해서 마스크 이미지를 생성할 수 있습니다. 이 스크립트에 기준 이미지의 경로(예: tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png)와 렌더링된 이미지를 가리키는 경로를 전달하면 됩니다.

예시:

scripts/generate_test_mask_image.py tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png /tmp/path_to_rendered_image.png

다음과 같이 테스트 이름의 일부분을 전달해서 기준 이미지를 가리키는 경로를 축약할 수 있습니다:

scripts/generate_test_mask_image.py annotation_fillstyle /tmp/path_to_rendered_image.png

(이 축약 경로는 테스트 이름의 일부분과 일치하는 이미지가 단 하나 존재하는 경우에만 작동합니다. 여러 개가 발견된 경우 기준 이미지를 가리키는 전체 경로를 제공해야 할 것입니다.)

이 스크립트는 렌더링 이미지의 HTTP URL도 받기 때문에, CDash 결과 페이지에서 렌더링된 이미지의 URL을 직접 복사해서 스크립트에 전달할 수 있습니다.

마스크 이미지를 생성할 때 주의하십시오 — 항상 생성된 마스크 이미지를 살펴보고 이미지에 하얀색 영역이 있는지 검토해야 합니다. 이 픽셀들을 무시할 것이기 때문에, 이 하얀색 영역이 기준 이미지의 중요한 부분들의 위치에 있지는 않은지 확인하십시오. 그러지 않으면 여러분의 단위 테스트가 의미 없게 될 것입니다!

마찬가지로, 여러분이 테스트에서 이미지의 어떤 영역을 의도적으로 제외하고 싶은 경우 해당 영역을 직접 “백화(白化)”시킬 수도 있습니다. 예를 들면 (범례 테스트처럼) 심볼과 텍스트를 혼합해서 렌더링한 이미지를 테스트하는 경우 이 방법이 유용합니다. 단위 테스트는 렌더링된 텍스트를 테스트하도록 설계되지 않았기 때문에, 플랫폼 간 텍스트 렌더링 차이 때문에 테스트에 영향을 주고 싶지 않을 것입니다.

QGIS 단위 테스트에서 이미지들을 비교하려면 QgsMultiRenderChecker 클래스 또는 그 하위 클래스들 가운데 하나를 사용해야 합니다.

테스트의 견고성을 높이기 위한 도움말을 몇 가지 소개합니다:

  1. 가능한 경우 위신호 제거(anti-aliasing)를 비활성화시키십시오. 플랫폼 간 렌더링 차이를 최소화해줍니다.

  2. 여러분의 기준 이미지가 “두툼한지” 확인하십시오 — 예를 들어 1픽셀 너비 라인 또는 기타 세밀한 피처를 사용하지 말고, 크고 굵은 글꼴을 (크기 14포인트 이상을 권장합니다) 사용하십시오.

  3. 테스트가 미묘하게 크기가 다른 이미지를 생성하는 경우가 있습니다. (예를 들면 이미지 크기가 플랫폼 간 차이에 영향을 받는 글꼴 렌더링 크기에 따라 달라지는 범례 렌더링 테스트가 그렇습니다.) 이를 고려하려면, QgsMultiRenderChecker::setSizeTolerance() 메소드를 사용해서 렌더링된 이미지의 너비와 높이가 기준 이미지와 비교해서 달라도 되는 최대 픽셀 개수를 지정하십시오.

  4. 기준 이미지에 투명한 배경을 사용하지 마십시오. (CDash가 지원하지 않습니다.) 그 대신 기준 이미지 배경에 바둑무늬 패턴을 그리는 QgsMultiRenderChecker::drawBackground() 메소드를 사용하십시오.

  5. 글꼴이 필요한 경우, QgsFontUtils::standardTestFontFamily() 메소드에 지정된 글꼴(“QGIS Vera Sans”)을 사용하십시오.

트래비스가 새 이미지에 대한 (예를 들어 위신호 제거 또는 글꼴 차이로 인한) 오류를 보고하는 경우, 로컬 테스트 마스크 이미지를 업데이트할 때 parse_dash_results.py 스크립트가 도움을 줄 수 있습니다.

6.4. CMakeLists.txt에 단위 테스트 추가하기

빌드 시스템에 여러분의 단위 테스트를 추가하려면 그냥 테스트 디렉터리에 있는 CMakeLists.txt 파일을 열고 기존 테스트 블록 가운데 하나를 복사한 다음 다음과 같이 해당 블록을 여러분의 테스트 클래스 이름으로 바꿔주기만 하면 됩니다:

# QgsRasterLayer test
ADD_QGIS_TEST(rasterlayertest testqgsrasterlayer.cpp)

6.4.1. ADD_QGIS_TEST 매크로 설명

다음 줄들을 간단히 훑어보면서 각 줄이 어떤 작업을 하는지 설명하겠습니다. 그러나 관심이 없을 경우, 그냥 앞 부분에서 설명한 단계를 따르십시오.

MACRO (ADD_QGIS_TEST testname testsrc)
SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})
SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})
ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)
TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)
SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)
ENDMACRO (ADD_QGIS_TEST)

개별 줄들을 좀 더 자세히 살펴봅시다. 먼저 테스트 용 소스 목록을 정의합니다. 이 예시에서는 소스 파일이 하나밖에 없기 때문에 (클래스 선언과 정의가 동일한 파일에 있다는, 앞에서 설명한 방법론에 따라) 다음과 같은 단순한 선언문으로 정의합니다:

SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})

테스트 클래스를 Qt MOC(Meta Object Compiler)을 통해 실행해야 하기 때문에, 이를 이뤄줄 몇 줄을 작성해줘야 합니다:

SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})

그 다음 CMake에 테스트 클래스로부터 실행 파일을 만들어야만 한다고 알려줍니다. 클래스 구현의 마지막 줄에 대해 설명한 앞 부분에서, (다른 일들 중에서도) 클래스를 실행 파일로 컴파일할 수 있는 주 메소드를 생성하도록 테스트 클래스에 MOC 산출물을 직접 include 시켰다는 사실을 기억하십시오.

ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)

다음으로 모든 라이브러리 의존성을 지정해줘야 합니다. 이 시점에서 클래스들은 포괄적인(catch-all) QT_LIBRARIES 의존성으로 구현되어 있지만 각 클래스에만 필요한 특정 Qt 라이브러리로 대체하기 위해 작업할 것입니다. 물론 단위 테스트가 요구하는 관련 QGIS 라이브러리를 가리키는 링크도 필요합니다.

TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)

그 다음 CMake에 QGIS 바이너리 자체와 동일한 위치에 테스트를 설치하라고 알려줍니다. 앞으로 소스 트리 안에서 테스트를 직접 실행할 수 있도록 이 부분은 향후 제거할 계획입니다.

SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)

마지막으로 앞의 예시는 CMake/CTest를 사용해서 테스트를 등록하는 데 ADD_TEST 를 사용합니다. 여기에서 아주 멋진 마법이 일어납니다 — CTest를 사용해서 클래스를 등록하는 것입니다. 이 장의 첫 부분에서 소개했던 개요를 다시 떠올려보면, QtTest와 CTest를 둘 다 함께 사용합니다. QtTest는 테스트 단위에 주 메소드를 추가하고 클래스 내부에서 테스트 메소드들을 호출하는 작업을 처리합니다. 조건을 사용하는 테스트가 실패하는 경우 테스트에 사용할 수 있는 QVERIFY 같은 몇몇 매크로들도 제공합니다. QtTest 단위 테스트에서 나온 산출물이 여러분이 명령줄에서 실행할 수 있는 실행 파일입니다. 하지만 테스트 스위트가 있고 각각의 실행 파일을 차례대로 실행하길 원하는 경우, 또는 더 나은 방법으로 실행 중인 테스트들을 빌드 프로세스로 통합하길 바라는 경우 사용하는 것이 CTest입니다.

6.5. 단위 테스트 빌드하기

단위 테스트를 빌드하려면 CMake 환경설정에 ENABLE_TESTS=true 를 넣어주기만 하면 됩니다. 두 가지 방법이 있습니다:

  1. ccmake .. (또는 윈도우인 경우 cmakesetup ..) 를 실행한 다음 쌍방향 작업을 통해 ENABLE_TESTS 플래그를 ON 으로 설정하십시오.

  2. 예를 들어 cmake -DENABLE_TESTS=true .. 와 같이 CMake에 명령줄 플래그를 추가하십시오.

그 외에는 그냥 평소대로 QGIS를 빌드하면 테스트도 빌드될 것입니다.

6.6. 테스트 실행

테스트를 실행하는 가장 단순한 방법은 일반적인 빌드 프로세스의 일부로써 실행하는 것입니다:

make && make install && make test

make test 명령어는 앞에서 설명했던 ADD_TEST CMake 명령어를 사용해서 등록한 각 테스트를 실행할 CTest를 호출합니다. make test 를 실행해서 나오는 일반적인 산출물은 다음과 같이 보일 것입니다:

Running tests...
Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
## 13 Testing qgis_applicationtest***Exception: Other
## 23 Testing qgis_filewritertest *** Passed
## 33 Testing qgis_rasterlayertest*** Passed

## 0 tests passed, 3 tests failed out of 3

The following tests FAILED:
## 1- qgis_applicationtest (OTHER_FAULT)
Errors while running CTest
make: *** [test] Error 8

테스트가 실패하는 경우, 실패한 이유를 더 자세히 알아보기 위해 ctest 명령어를 사용할 수 있습니다. 실행하고자 하는 테스트에 대한 정규 표현식을 지정하려면 -R 옵션을 사용하고, 서술적인 자세한 산출물을 얻으려면 -V 옵션을 사용하십시오:

$ ctest -R appl -V

Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
Constructing a list of tests
Done constructing a list of tests
Changing directory into /Users/tim/dev/cpp/qgis/build/tests/src/core
## 13 Testing qgis_applicationtest
Test command: /Users/tim/dev/cpp/qgis/build/tests/src/core/qgis_applicationtest
********* Start testing of TestQgsApplication *********
Config: Using QTest library 4.3.0, Qt 4.3.0
PASS : TestQgsApplication::initTestCase()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
PASS : TestQgsApplication::getPaths()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
QDEBUG : TestQgsApplication::checkTheme() Checking if a theme icon exists:
QDEBUG : TestQgsApplication::checkTheme()
/Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis/themes/default//mIconProjectionDisabled.png
FAIL!: TestQgsApplication::checkTheme() '!myPixmap.isNull()' returned FALSE. ()
Loc: [/Users/tim/dev/cpp/qgis/tests/src/core/testqgsapplication.cpp(59)]
PASS : TestQgsApplication::cleanupTestCase()
Totals: 3 passed, 1 failed, 0 skipped
********* Finished testing of TestQgsApplication *********
-- Process completed
***Failed

## 0 tests passed, 1 tests failed out of 1

The following tests FAILED:
## 1- qgis_applicationtest (Failed)
Errors while running CTest

6.6.1. 개별 테스트 실행하기

C++ 테스트는 일반적인 응용 프로그램입니다. 다른 모든 실행 파일들처럼 빌드 폴더에서 테스트를 실행할 수 있습니다.

$ ./output/bin/qgis_dxfexporttest

********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS   : TestQgsDxfExport::initTestCase()
PASS   : TestQgsDxfExport::testPoints()
PASS   : TestQgsDxfExport::testLines()
...
Totals: 19 passed, 4 failed, 0 skipped, 0 blacklisted, 612ms
********* Finished testing of TestQgsDxfExport *********

이런 테스트들은 명령줄 인자 도 받습니다. 이 인자들을 사용하면 테스트의 특정 하위 집합을 실행할 수 있습니다:

$ ./output/bin/qgis_dxfexporttest testPoints
********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS   : TestQgsDxfExport::initTestCase()
PASS   : TestQgsDxfExport::testPoints()
PASS   : TestQgsDxfExport::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 272ms
********* Finished testing of TestQgsDxfExport *********

6.6.2. 단위 테스트 디버그 작업

C++ 테스트

C++ 단위 테스트의 경우, QtCreator가 실행 대상을 자동으로 추가하기 때문에 디버그 작업자에서 테스트를 시작할 수 있습니다.

Projects 메뉴로 가서 Build & Run ► Run 탭을 선택하면, 디버그 작업자에서 .cpp 파일 안에서 테스트의 하위 집합을 실행할 수 있게 해줄 명령줄 파라미터도 지정할 수 있습니다.

파이썬 테스트

GDB(GNU Debugger)를 사용하면 QtCreator에서 파이썬 단위 테스트도 시작할 수 있습니다. 이러러면 Projects 메뉴로 가서 Build & Run ► Run 탭을 선택해야 합니다. 그 다음 /usr/bin/python3 실행 파일과 예를 들어 /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattributeformeditorwidget.py 와 같은 단위 테스트 파이썬 파일의 경로로 설정된 명령줄 인자를 사용해서 새 Run configuration 을 추가하십시오.

이제 다음 새로운 변수 3개를 추가해서 Run Environment 도 변경하십시오:

변수

PYTHONPATH

[build]/output/python/:[build]/output/python/plugins:[source]/tests/src/python

QGIS_PREFIX_PATH

[build]/output

LD_LIBRARY_PATH

[build]/output/lib

[build] 는 여러분의 빌드 디렉터리로 대체하고 [source] 는 여러분의 소스 디렉터리로 대체하십시오.

6.6.3. 재미있게 놀아보세요

이것으로 QGIS에 단위 테스트를 작성하는 데에 대한 장을 마칩니다. 여러분이 새로운 기능을 테스트하기 위한 테스트를 작성하고 코드 회귀에 대해 확인해보는 습관을 들이기를 바랍니다. 테스트 시스템의 일부 측면은 (특히 CMakeLists.txt 부분은) 아직 개발 중이기 때문에 테스트 작업 프레임워크는 실제로 플랫폼 독립적인 방식으로 작동합니다.