Ich werde die Datenbank in SQLite implementieren. Die Datenbank sollte zwei Tabellen enthalten: für Projekte und für Aufgaben. Die Tabellen in einer Datenbank sind voneinander unabhängig. Alle Datenbank- und Tabellenvariablen sind aus Konsistenzgründen global.
// 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
Betrachten Sie nun die Implementierung der obigen Methoden.
Beginnen wir mit der Initialisierung der Datenbank.
// 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; } }
Schauen wir uns nun eine der Datenbankmethoden genauer an.
// Метод добавления новой задачи в базу данных 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; }
Die verbleibenden Methoden sind in das Bild und die Ähnlichkeit geschrieben, daher werde ich ihren Code weglassen.
Fahren wir mit dem Testen der schriftlichen Methoden fort.
Bevor wir mit dem Schreiben des Testcodes beginnen, legen wir die Datei DataBase.pri in den Ordner mit dem Projektcode, um das Einschließen der DataBase-Klasse in Tests zu vereinfachen.
# DataBase.pri SOURCES += \ $$PWD/database.cpp HEADERS += \ $$PWD/database.h
Wir erstellen ein Teilprojekt Test_DataBase und eine gleichnamige Klasse zum Testen und binden auch common.pri ein.
Qt Creator hat die Fähigkeit, Tests automatisch zu erstellen, aber ich verwende sie nicht, da Sie einen Test nur von Qt Creator selbst ausführen können. In meinem Fall wird eine separate ausführbare Datei abgerufen. Wenn Sie sie mit den entsprechenden Befehlszeilenparametern ausführen, können Sie einen Testbericht in Form einer XML-Datei erhalten.
# 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
Betrachten wir im Detail nur den ersten Test-Slot. Der Rest ist mehr oder weniger gleich.
// 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 - внимательно читать вывод теста, там содержится вся информация об ошибке. // И помнить главное: ошибка может быть в основном коде, а может - в коде теста... }
Als Testergebnis erhalten wir:
Totals: 13 passed, 0 failed, 0 skipped, 0 blacklisted, 1113ms ********* Finished testing of Test_DataBase *********
Eigentlich was verlangt wurde. Es ist erwähnenswert, dass pass zwei Standard-Slots enthält, die zu Beginn und am Ende des Methodentests ausgeführt werden: Dies sind initTestCase() bzw. cleanupTestCase().
Lassen Sie uns die Daten der Schönheit halber in eine XML-Datei entladen.
Test_DataBase.exe –xml –o <путь к папке с отчётами>\report.xml
Ich werde es mit Hilfe eines selbst entwickelten TRViewer-Dienstprogramms öffnen.
Die Datenbank wurde fertiggestellt.
Statt Schluss
Auf eine gute Weise müssen Sie das Gegenteil tun - zuerst Tests schreiben, dann die Methoden selbst. Ich habe zuerst Methoden geschrieben, dann Tests. Und dafür habe ich ein wenig bezahlt: Ich musste mehrere Methoden entwickeln, die ursprünglich nicht geplant waren. Aber das Schreiben von Tests, wo immer möglich, ist notwendig. Die praktische Erfahrung dieses Projektes hat gezeigt, dass wenn ein neues Feld unvorsichtig zur Datenbank hinzugefügt wird, es überall kaputt gehen kann. Das Auffinden des Problembereichs ohne Tests wäre viel schwieriger.
Отличная статья.
Вопрос по поводу утилиты TRViewer, она в репозитории есть? Или ее надо компилить самому под дистриб?
Есть комментарий по вашему коду. Лучше бы вместо глобальных переменных в стиле Си, то есть с использоавнием extern, написали бы статические переменные в рамках класса. IMHO - это будет выглядеть более лаконично, и если не ошибаюсь, то не потребует каждый раз объявлять extern в других файлах.
По факту, если у вас есть глобальная переменная, то вам придётся в каждом другом файле объявлять её как extern. Если у вас есть 50 таких переменных, которые активно используются, то придёдтся попотеть, чтобы не забыть их объявлять везде, где потребуется.
Проще написать класс, со статическими переменными, и потом просто подключать его заголовочный файл там, где потребуется.
Это будет выглядеть так
Header
CPP
В репозиторий могу сегодня вечером выложить.
"Или ее надо компилить самому под дистриб?" Тут я не совсем понимаю, что вы имеете ввиду. Я выложу в репозиторий исходный код утилиты, и всё.
"Не потребует каждый раз объявлять extern в других файлах". И так не требует. У меня в тестовом классе эти переменные используются без дополнительного объявления. Так же объявил их в cpp-файле один раз, и всё.
Хорошо, хотя конечно это С, а не С++ ))))
Но если вдруг будут проблемы, то решение через класс со статическими переменными вы видели ))
Вот ссылка: https://github.com/iscander-che/TestReportViewer .