Базу данных я буду реализовывать на 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 .