Archiwa tagu: testy jednostkowe

Jenkins – konfiguracja projektu Qt [Windows]

Jenkins jest narzędziem do automatycznego budowania i testowania aplikacji. Ten wpis opisuje jak skonfigurować Jenkinsa do budowania i testowaniu aplikacji napisanych w C++ i Qt.
Docelowo Jenkins będzie pobierał projekt z gita, zbuduje go i odpali projekt z testami jednostkowymi, który jest aplikacją konsolową.

Nowy projekt.
Z menu na stronie głównej wybieramy New Item, podajemy nazwę projektu i klikamy Freestyle project i OK.

W karcie Source Code Management wybieramy git i podajemy adres repozytorium, np.
https://github.com/ololuki/Nonograms

Dobrze jest zaznaczyć checkbox „Delete workspace before build starts” (wymagany plugin „Workspace Cleanup”). Dzięki temu projekt zostanie za każdym razem wyczyszczony i zbudowany od zera, co pozwoli uniknąć dziwnych błędów kompilacji w przypadku dużych zmian w projekcie.

W karcie build dodajemy Add build step -> Execute Windows batch command
Dla kompilatora Visual C++ 2015 x64:

:: Konfiguracja środowiska
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64

:: Uruchomienie qmake
"D:\Qt\5.8\msvc2015_64\bin\qmake.exe" "nonograms.pro" -spec win32-msvc2015 "CONFIG+=debug" "CONFIG+=qml_debug"

:: Przerwanie skryptu w przypadku błędu
if errorlevel 1 (
   echo Error: %errorlevel%
   exit /b %errorlevel%
)

:: Kompilacja projektu
"D:\Qt\Tools\QtCreator\bin\jom.exe" -f "Makefile"

:: Przerwanie skryptu w przypadku błędu
if errorlevel 1 (
   echo Error: %errorlevel%
   exit /b %errorlevel%
)

:: Dodanie bibliotek Qt do PATHa, aby uruchomić aplikację z testami
SET PATH=%PATH%;D:\Qt\5.8\msvc2015_64\bin

:: Uruchomienie aplikacji konsolowej z testami
"bin\NonogramsTests.exe"

Zadania w pliku wsadowym w cmd.exe są domyślnie wykonywane linia po linii bez sprawdzania błędów, więc jeśli jedno z zadań się nie powiedzie to pozostałe i tak mogą zostać wykonane. Trzeba więc ręcznie zadbać o zatrzymanie skryptu w przypadku błędu jednego z zadań poprzez sprawdzenie wartości zmiennej „errorlevel”.

Automatyczne sprawdzanie zmian w repozytorium.

Aby Jenkins automatycznie sprawdzał czy nie ma nowych zmian można użyć zakładki „Build Triggers”, zaznaczyć checkbox „Poll SCM” i w polu „Schedule” wpisać:

H/15 * * * *

gdzie 15 oznacza liczbę minut pomiędzy kolejnymi odpytaniami repozytorium.

Ręczne budowanie konkretnej wersji.

Aby umożliwić ręczne budowanie konkretnej wersji projektu za pomocą Jenkinsa w zakładce „General” zaznaczamy opcję „This project is parametrized” i w polu Name podajemy nazwe parametru, np. „commitId” – w naszym przepadku jest to hash commitu. Aby umożliwić budowanie wersji o podanym hashu w zakładce „Source Code Management” w polu „Branches to build” wpisujemy:

${commitId}

Od tej chwili po wejściu do projektu zamiast przycisku „Build Now” pojawi się przycisk „Build with Parameters” umożliwiający wpisanie hasha commita do zbudowania.

Qt Test – przykładowy projekt z testami wykorzystujący bibliotekę statyczną dla wspólnego kodu

Tworząc bardziej rozbudowany projekt aplikacji wykorzystujący testy jednostkowe można natknąć się na problem struktury projektu. Pierwszym podejściem może być stworzenie dwóch projektów: aplikacji oraz testów. W tym rozwiązaniu pliki z kodem produkcyjnym dodajemy do projektu aplikacji, natomiast pliki testów do projektu test. Jednak projekt testowy także musi mieć dostęp do plików nagłówkowych oraz źródłowych aplikacji. Jeśli potrzebne pliki projektu app dodamy także do projektu test to zauważymy dwa problemy:
1. Każdy z tych plików źródłowych będzie kompilowany dwa razy
2. Qt Creator wyświetli ostrzeżenie „Multiple parse contexts are available for this file. Choose preferred one from the editor toolbar.”

Aby uniknąć powyższych problemów warto kompilować część wspólną do biblioteki statycznej a następnie linkować tę bibliotekę w projektach app i test. W poniższym przykładzie biblioteka statyczna (projekt core) zawiera klasy, które są jednocześnie używane w projekcie app i testowane w projekcie tests.

Link do kompletnego projektu: Qt-simple-examples/ProjectStructureQTestWithStaticLib

Qt Unit Test – projekt z kilkoma klasami testowymi

Mając chwilowe problemy z dotychczasowym frameworkiem do testów jednostkowych postanowiłem przyjrzeć się frameworkowi wbudowanemu w Qt: Qt Test.

Pierwszy problem pojawia się gdy po stworzeniu nowego projektu Qt Unit Test w Qt Creatorze próbujemy go odpalić w konfiguracji debug. Kończy się to błędem „moc: Too many input files specified: „. Problem wynika ze spacji w ścieżce. Można go rozwiązać usuwając jedną linię z pliku *.pro:

#DEFINES += SRCDIR=\\\"$$PWD/\\\"

Druga sprawa dotyczy architektury frameworka Qt Unit Test. Domyślny projekt testowy stworzony w Qt Creatorze zawiera tylko jedną klasę testową. W plikach źródłowych Qt każda klasa testowa jest osobnym projektem, a te projekty połączone są w strukturę drzewa za pomocą projektów subdirs. Domyślnie funkcja main w projektach testowych jest implementowana poprzez makro

QTEST_APPLESS_MAIN(UnittestTest)

i pozwala na dodanie tylko jednej klasy z testami jednostkowymi. Aby móc stworzyć projekt zawierający kilka klas z testami i jedną funkcją main wystarczy tę funkcję main ręcznie zaimplementować:

int main()
{
	QVector<QObject*> tests;
	
	tests.append(new FooTest);
	tests.append(new BarTest);
	
	int result = 0;
	for (int i = 0; i < tests.length(); i++)
	{
		result = QTest::qExec(tests[i]);
		if (result) break;
	}
	qDeleteAll(tests);
	return result;
}

Link do kompletnego projektu: Qt-simple-examples/QTestMultipleTestClasses

Innym sprawą, która skłoniła mnie do przejścia z Catcha na Qt Unit Test był problem związany z obsługą biblioteki Catch przez Qt Creatora. Podczas zmiany nazwy metody lub klasy za pomocą polecenia Refactor -> Reaname Symbol Under Cursor, Qt Creator widzi jedynie użycia zmienianej metody lub klasy w projekcie aplikacji, natomiast nie widzi wystąpień tych nazw w projekcie test (który używa Catcha). Gdy w projekcie test użyta zostanie biblioteka Qt Test to Qt Creator nie będzie miał już problemów i zmieni nazwy wszystkich wystąpień edytowanej nazwy.

Qt test – unresolved external symbol metaObject – rozwiązanie

Dotychczasowy framework do testów jednostkowych Catch nie chciał zbytnio współpracować z klasami dziedziczącymi po QObject. Przy próbie kompilacji zwracał błąd: „LNK2001: unresolved external symbol „public: virtual struct QMetaObject const * __cdecl BlocksDescriptionField::metaObject(void)const”. Rozwiązaniem okazało się dodanie plików nagłówkowych w pliku *.pro:

...
HEADERS += ../app/field/BlocksDescriptionField.h
...

Catch – problemy przy testowaniu plików *.cpp – unresolved external symbol

Pojawiły się pewne problemy przy testowaniu plików *.cpp. Dotychczas wszystkie pliki aplikacji objęte testami były zdefiniowane w plikach nagłówkowych *.h, więc problem się nie ujawniał, bo nie był potrzebny żaden kod z plików *.cpp. Dzisiaj postanowiłem napisać test do dość rozbudowanej klasy, której konstruktor został zdefiniowany w pliku *.cpp i niestety pojawił się błąd „unresolved external symbol”. Trochę czasu zajęło mi znalezienie przyczyny, którą był brak dołączonych plików *.cpp zawierających definicje konstruktorów i metod. Rozwiązaniem problemu było dołączenie plików *.cpp aplikacji do projektu test.pro.

Czytaj dalej

Testy jednostkowe w Qt Creatorze

W tym poście zostanie przedstawiona struktura projektu zawierającego aplikację oraz testy jednostkowe. Przedstawię dwa przykłady. Pierwszy z nich będzie oparty o wbudowaną bibliotekę QtTest, natomiast w drugim przykładzie zostanie wykorzystana biblioteka Catch, która jest lekka i składa się tylko z jednego pliku nagłówkowego.

Struktura projektu

TemplateProj struktura.png

Projekt (TemplateProj) składa się z dwóch podprojektów: standardowego projektu aplikacji (app) oraz projektu zawierającego testy jednostkowe (tests). Główny projekt (TemplateProj) to tak naprawdę tylko kontener na pozostałe projekty. Korzysta z szablonu TEMPLATE = subdirs i definiuje jedynie nazwy podkatalogów projektu. CONFIG += ordered informuje kompilator, że wymienione podkatalogi powinny być przetwarzane w takiej kolejności w jakiej są wymienione. Zapewnia to poprawną kompilację, ponieważ projekt tests korzysta z elementów projektu app. Zawartość pliku TemplateProj:

TEMPLATE = subdirs

CONFIG += ordered

SUBDIRS = \
    app \
    tests

Projekt app jest generalnie standardowym projektem aplikacji, takim jaki można stworzyć w Qt Creatorze z szablonu nowy projekt. Wpis CONFIG += console umożliwia prawidłowe działanie programu, gdy odpalamy program w terminalu zamiast domyślnego okna application output. Brak tego wpisu powoduje, że po odpaleniu aplikacji okno konsoli jest puste i wyświetla tylko standardowe „Press Return to close this window”. Zmiana opcji „Run in terminal” jest dostępna w karcie Projects → Run Settings osobno dla każdej aplikacji. DESTDIR = $$PWD/../build ustawia katalog z plikami wynikowymi na build w katalogu głównym projektu. Można tę linię pominąć, wtedy pliki wynikowe pojawią się w domyślnych katalogach debug / release.

QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += console

TARGET = TemplateProj
TEMPLATE = app

DESTDIR = $$PWD/../build

HEADERS += \
    Calculator.h

SOURCES += \
    main.cpp \
    Calculator.cpp

W głównym projekcie app dodajmy plik main.cpp oraz przykładową klasę Calculator:

#include 
#include 

int main(int argc, char *argv[])
{
	std::cout << "Example test project" << std::endl;
	Calculator calc;
	std::cout << "2 + 6 = " << calc.add(2,6) << std::endl;
	
	return 0;
}
#ifndef CALCULATOR_H
#define CALCULATOR_H

class Calculator
{
public:
	int add(int a, int b);
};

#endif // CALCULATOR_H
#include "Calculator.h"

int Calculator::add(int a, int b)
{
	return a + b;
}

Do tego miejsca wszystkie elementy są niezależne od użytej technologii testów. Natomiast projekt z testami jest już dostosowywany do danej biblioteki testowej.

QtTest

QtTest to biblioteka dostarczana wraz z całym pakietem Qt, więc stworzenie projektu zawierającego testy jednostkowe jest możliwe bez wprowadzania dodatkowych zależności od zewnętrznych bibliotek.
Najprostszy plik projektu tests dla QtTest wygląda następująco:

QT += testlib

TARGET = TemplateProjUnitTests

SOURCES += TestMain.cpp \
    ../app/Calculator.cpp
    
INCLUDEPATH += \
    ../app

Plik TestMain.cpp (omówienie):

#include 
#include "Calculator.h"

class TestMain: public QObject
{
	Q_OBJECT
private slots:
	void adder();
};

void TestMain::adder()
{
	Calculator c;
	QCOMPARE(c.add(3,5), 8);
	QCOMPARE(c.add(3,10), 13);
}

QTEST_MAIN(TestMain)
#include "TestMain.moc"

Link do kompletnego projektu: Qt-simple-examples/ProjectStructureQTest

Catch

Catch to biblioteka testów dla C++. Jej zaletami są liberalna licencja, brak zależności od innych bibliotek oraz dystrybucja w postaci jednego pliku nagłówkowego.
Najprostszy plik projektu tests dla Catch wygląda następująco (chociaż działa także bez linijki TEMPLATE = app, ale została ona pozostawiona dla formalności):

QT += core

TARGET = TemplateProjUnitTests
TEMPLATE = app

CONFIG += console

SOURCES += TestMain.cpp \
    CalculatorTest.cpp \
    ../app/Calculator.cpp

INCLUDEPATH += \
    ../app \
    ../lib

Plik TestMain.cpp - odpowiada za prawidłowe wywołanie pozostałych plików źródłowych z testami. Zawiera w sobie definicje jednego makra i include biblioteki Catch. Makro CATCH_CONFIG_MAIN powinno zostać wywołane tylko w jednym miejscu projektu.

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

Właściwy plik z testami:

#include "catch.hpp"
#include "Calculator.h"

TEST_CASE("add test") {
	Calculator c;

	REQUIRE(c.add(3,5) == 8);
	REQUIRE(c.add(3,10) == 13);
}

Link do kompletnego projektu: Qt-simple-examples/ProjectStructureCatch
O strukturze projektu: link
Przykład aplikacji wykorzystującej bibliotekę Catch: link

Potencjalne problemy

  • qmake exited with code 2 - prawdopodobnie ścieżka projektu jest zbyt długa - wystarczy przenieść projekt do katalogu o krótszej ścieżce
  • inne błędy przy kompilacji - kliknąć prawym przyciskiem na projekcie → Run qmake a następnie Rebuild