Базу даних я реалізовуватиму на SQLite. У основі має бути розміщено дві таблиці: для проектів й у задач. Таблиці базі даних незалежні друг від друга. Усі змінні для бази даних та таблиць зроблені глобальними для однаковості.
// database.h #ifndef DATABASE_H #define DATABASE_H #include <QSql> #include <QSqlQuery> #include <QSqlError> #include <QSqlDatabase> #include <QDebug> #include <QApplication> // Настройки базы данных extern const QString typeDB; extern const QString hostNameDB; extern const QString nameDB; // Настройки таблицы проектов extern const QString projectTable; extern const QString idProjectCol; extern const QString visibleNameProjectCol; extern const QString manualPropertyCol; extern const QString archivePropertyCol; // Настройки таблицы задач extern const QString taskTable; extern const QString idTaskCol; extern const QString idTaskProjectCol; extern const QString openDateTaskCol; extern const QString typeTaskCol; extern const QString descriptionTaskCol; extern const QString statusTaskCol; extern const QString closeDateTaskCol; extern const QString revisionTaskCol; // 0 - с поддержкой СКВ, 1 - без поддержки СКВ enum ProjectVCS { WithVCS /* = 0 */, WithoutVCS /* = 1 */ }; // 0 - активный проект, 1 - проект в архиве enum ProjectArchive { Active /* = 0 */, Archive /* = 1*/ }; class DataBase { public: DataBase(); ~DataBase(); // Метод добавления новой задачи в базу данных bool insertNewTask(const int projectId, const QString& type, const QString& description); // Метод редактирования задачи из базы данных bool editTask(int id, const QString& type, const QString& description); // Метод определения существования задачи с соответствующим ID bool isExists(int id); // Метод удаления задачи из базы данных bool deleteTask(int id); // Метод обновления состояния задачи bool updateStatusTask(int id, const QString& status); // Метод закрытия задачи bool closeTask(int id, const QString& revision); // Метод проверки, закрыты ли все задачи в проекте bool isClosedAllTasks(int projectId); // Метод добавления нового проекта в базу данных bool insertNewProject(const QString& nameProject, int manual); // Метод определения проекта с поддержкой или без поддержки СКВ int isManual(int id); // Метод определения, в архиве проект или нет int isArchive(int id); // Метод архивирования проекта bool archiveProject(int id); // Метод извлечения проекта из архива bool extractProject(int id); private: QSqlDatabase db; }; #endif // DATABASE_H
Тепер розглянемо реалізацію наведених методів.
Почнемо з ініціалізації бази даних.
// database.cpp #include "database.h" #include <QDate> #include <QStringList> // Настройки базы данных const QString typeDB = "QSQLITE"; const QString hostNameDB = "localhost"; const QString nameDB = "db.db"; // Настройки таблицы проектов const QString projectTable = "project_tbl"; const QString idProjectCol = "id_project"; const QString visibleNameProjectCol = "name_project"; const QString manualPropertyCol = "manual"; const QString archivePropertyCol = "archive"; // Настройки таблицы задач const QString taskTable = "task_tbl"; const QString idTaskCol = "id_task"; const QString idTaskProjectCol = "id_task_project"; const QString openDateTaskCol = "open_date"; const QString typeTaskCol = "type"; const QString descriptionTaskCol = "description"; const QString statusTaskCol = "status"; const QString closeDateTaskCol = "close_date"; const QString revisionTaskCol = "revision"; DataBase::DataBase() { // Создаём базу данных в формате SQLite db = QSqlDatabase::addDatabase(typeDB); // Задаём имя сервера базы данных db.setHostName(hostNameDB); // Размещаем файл базы данных в папке с приложением, чтобы не искать db.setDatabaseName(qApp->applicationDirPath() + "/" + nameDB); // Если база данных успешно открыта if(db.open()) qDebug() << "DB open"; // Если что-то пошло не так else qDebug() << "DB not open"; // Формируем SQL-запрос на создание таблицы проектов QSqlQuery queryTable; if(!queryTable.exec( "CREATE TABLE " + projectTable + " (" + idProjectCol + " INTEGER PRIMARY KEY AUTOINCREMENT, " + visibleNameProjectCol + " TEXT, " + manualPropertyCol + " INTEGER, " + archivePropertyCol + " INTEGER" ")")) // Если запрос выше не может быть выполнен { qDebug() << "Error create table " + projectTable; qDebug() << queryTable.lastError().text(); } // Если всё прошло успешно else { qDebug() << "Created table " + projectTable; } // Формируем SQL-запрос на создание таблицы задач if(!queryTable.exec( "CREATE TABLE " + taskTable + " (" + idTaskCol + " INTEGER PRIMARY KEY AUTOINCREMENT, " + idTaskProjectCol + " INTEGER, " + openDateTaskCol + " DATE, " + typeTaskCol + " TEXT, " + descriptionTaskCol + " TEXT, " + statusTaskCol + " TEXT, " + closeDateTaskCol + " DATE, " + revisionTaskCol + " TEXT" ")")) // Если запрос выше не может быть выполнен { qDebug() << "Error create table " + taskTable; qDebug() << queryTable.lastError().text(); } // Если всё прошло успешно else { qDebug() << "Created table " + taskTable; } }
Тепер докладно розглянемо один із методів бази даних.
// Метод добавления новой задачи в базу данных bool DataBase::insertNewTask(const int projectId, const QString& type, const QString& description) { // Формируем SQL-запрос на вставку данных QSqlQuery queryInsert; queryInsert.prepare( "INSERT INTO " + taskTable + " (" + idTaskProjectCol + ", " + openDateTaskCol + ", " + typeTaskCol + ", " + descriptionTaskCol + ", " + statusTaskCol + ") " "VALUES (:ProjectId, :Date, :Type, :Description, :Status)" ); queryInsert.bindValue(":ProjectId", projectId); queryInsert.bindValue(":Date", QDate::currentDate()); queryInsert.bindValue(":Type", type); queryInsert.bindValue(":Description", description); queryInsert.bindValue(":Status", "не активна"); // Если запрос не может быть выполнен if(!queryInsert.exec()) { qDebug() << "Error insert data into " + taskTable; qDebug() << queryInsert.lastError().text(); return false; } // Если всё прошло успешно else { qDebug() << "Inserted data into " + taskTable; return true; } return false; }
Інші методи написані за образом і подобою, тому їхній код я опущу.
Переходимо до тестування написаних методів.
Перед тим як почати писати код тестів, покладемо в папку з кодом проекту файл DataBase.pri, щоб зручніше підключати клас DataBase в тести.
# DataBase.pri SOURCES += \ $$PWD/database.cpp HEADERS += \ $$PWD/database.h
Створюємо підпроект Test_DataBase та однойменний клас для тестування, а також підключаємо common.pri.
У Qt Creator є можливість автоматизовано створювати тести, але я нею не користуюся, оскільки запустити тест можна лише з Qt Creator. У моєму випадку виходить окремий файл, запустивши який з відповідними параметрами командного рядка, можна отримати звіт про тестування у вигляді xml-файлу.
# Test_DataBase.pro include(../../common.pri) include(../../ICTrackerServer/DataBase.pri) QT += core gui testlib sql CONFIG += c++11 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = Test_DataBase TEMPLATE = app DEFINES += QT_DEPRECATED_WARNINGS SOURCES += \ main.cpp \ test_database.cpp HEADERS += \ test_database.h
// main.cpp #include "test_database.h" #include <QApplication> #include <QTest> int main(int argc, char* argv[]) { QCoreApplication a(argc, argv); QTest::qExec(new Test_DataBase(), argc, argv); return 0; }
// test_database.h #ifndef TEST_DATABASE_H #define TEST_DATABASE_H #include "../../ICTrackerServer/database.h" #include <QObject> #include <QTest> class Test_DataBase : public QObject { Q_OBJECT public: explicit Test_DataBase(QObject* parent = nullptr); private: // Объявляем указатель на объект базы данных DataBase* db; private slots: // Тестирование метода bool insertNewTask(const QDate& openDate, const QString& type, // const QString& description) void insertNewTask(); // Тестирование метода bool editTask(int id, const QString& type, const QString& description) void editTask(); // Тестирование метода bool isExists(int id) void isExists(); // Тестирование метода bool deleteTask(int id) void deleteTask(); // Тестирование метода bool updateStatusTask(int id, const QString& status) void updateStatusTask(); // Тестирование метода bool closeTask(int id, const QDate& closeDate, const QString& revision) void closeTask(); // Тестирование метода bool insertNewProject(const QString& nameProject, int manual) void insertNewProject(); // Тестирование метода int isManual(int id) void isManual(); // Тестирование метода int isArchive(int id) void isArchive(); // Тестирование метода bool archiveProject(int id) void archiveProject(); // Тестирование метода bool extractProject(int id) void extractProject(); }; #endif // TEST_DATABASE_H
Розглянемо докладно лише перший слот тестування. Інші більш-менш аналогічні йому.
// test_database.cpp #include "test_database.h" #include <QDate> #include <QDebug> Test_DataBase::Test_DataBase(QObject* parent) : QObject(parent) { // Создаём новый объект DataBase. Помним, что создаётся база данных и таблицы. db = new DataBase; } void Test_DataBase::insertNewTask() { // Формируем данные для тестирования int id = 1; int projectId = 1; QDate date = QDate::currentDate(); QString type = "bug"; QString description = "Description 1"; QString status = "не активна"; QDate closeDate = QDate(0, 0, 0); QString revision = ""; // Для удобства сравнения помещаем данные в список QVariant QVariantList query; query << id << projectId << date << type << description << status << closeDate << revision; // Добавляем данные в базу данных db->insertNewTask(projectId, type, description); // Создаём список для результатов запроса к базе данных QVariantList result; // Формируем запрос к базе данных QSqlQuery querySelect; querySelect.prepare( "SELECT * FROM " + taskTable + " WHERE " + idTaskCol + " = :Id"); querySelect.bindValue(":Id", id); // Если запрос не может быть выполнен if(!querySelect.exec()) { qDebug() << "Test_DataBase::insertNewTask() Error select from table " + taskTable; qDebug() << querySelect.lastError().text(); } // Если всё прошло успешно else { while(querySelect.next()) { // Заполняем список результатами запроса result << querySelect.value(0).toInt(); result << querySelect.value(1).toInt(); result << querySelect.value(2).toDate(); result << querySelect.value(3).toString(); result << querySelect.value(4).toString(); result << querySelect.value(5).toString(); result << querySelect.value(6).toDate(); result << querySelect.value(7).toString(); } } // Сравниваем исходные данные и результат. QCOMPARE(query, result); // Исход для теста должен быть PASS. // Если исход FAIL - внимательно читать вывод теста, там содержится вся информация об ошибке. // И помнить главное: ошибка может быть в основном коде, а может - в коде теста... }
На виході тестування отримуємо:
Totals: 13 passed, 0 failed, 0 skipped, 0 blacklisted, 1113ms ********* Finished testing of Test_DataBase *********
Власне, що й потрібно. Варто зазначити, що в passed входять два дефолтні слоти, які виконуються на початку і в кінці тестування методів: це initTestCase() і cleanupTestCase() відповідно.
Для краси вивантажимо дані в XML-файл.
Test_DataBase.exe –xml –o <путь к папке с отчётами>\report.xml
Відкрию його за допомогою самостійно розробленої утиліти TRViewer.
Роботу над базою даних закінчено.
Замість укладання
По-хорошому, треба робити навпаки – спочатку писати тести, потім самі методи. Я спочатку писав методи, потім випробування. І трохи за це поплатився: довелося розробити кілька методів, які спочатку не планувалися. Але писати тести там, де це можливо – необхідно. Практичний досвід цього проекту показав, що при неуважному додаванні нового поля до бази даних вона може поламатися будь-де. Знайти проблемне місце без тестування було б важче.
Отличная статья.
Вопрос по поводу утилиты TRViewer, она в репозитории есть? Или ее надо компилить самому под дистриб?
Есть комментарий по вашему коду. Лучше бы вместо глобальных переменных в стиле Си, то есть с использоавнием extern, написали бы статические переменные в рамках класса. IMHO - это будет выглядеть более лаконично, и если не ошибаюсь, то не потребует каждый раз объявлять extern в других файлах.
По факту, если у вас есть глобальная переменная, то вам придётся в каждом другом файле объявлять её как extern. Если у вас есть 50 таких переменных, которые активно используются, то придёдтся попотеть, чтобы не забыть их объявлять везде, где потребуется.
Проще написать класс, со статическими переменными, и потом просто подключать его заголовочный файл там, где потребуется.
Это будет выглядеть так
Header
CPP
В репозиторий могу сегодня вечером выложить.
"Или ее надо компилить самому под дистриб?" Тут я не совсем понимаю, что вы имеете ввиду. Я выложу в репозиторий исходный код утилиты, и всё.
"Не потребует каждый раз объявлять extern в других файлах". И так не требует. У меня в тестовом классе эти переменные используются без дополнительного объявления. Так же объявил их в cpp-файле один раз, и всё.
Хорошо, хотя конечно это С, а не С++ ))))
Но если вдруг будут проблемы, то решение через класс со статическими переменными вы видели ))
Вот ссылка: https://github.com/iscander-che/TestReportViewer .