IscanderChe
IscanderChe23. Juli 2019 03:17

Einfaches Tracker-Projekt. Teil 3: Server. Datenbank und ihre Tests

Inhalt

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.

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Stabiles Hosting des sozialen Netzwerks EVILEG. Wir empfehlen VDS-Hosting für Django-Projekte.

Magst du es? In sozialen Netzwerken teilen!

BlinCT
  • 23. Juli 2019 04:32

Отличная статья.
Вопрос по поводу утилиты TRViewer, она в репозитории есть? Или ее надо компилить самому под дистриб?

Evgenii Legotckoi
  • 23. Juli 2019 04:32

Есть комментарий по вашему коду. Лучше бы вместо глобальных переменных в стиле Си, то есть с использоавнием extern, написали бы статические переменные в рамках класса. IMHO - это будет выглядеть более лаконично, и если не ошибаюсь, то не потребует каждый раз объявлять extern в других файлах.

По факту, если у вас есть глобальная переменная, то вам придётся в каждом другом файле объявлять её как extern. Если у вас есть 50 таких переменных, которые активно используются, то придёдтся попотеть, чтобы не забыть их объявлять везде, где потребуется.

Проще написать класс, со статическими переменными, и потом просто подключать его заголовочный файл там, где потребуется.

Это будет выглядеть так

Header

class DataBaseSettings
{
public:
    static const QString NAME;
    static const QString HOSTNAME;
}

CPP

#include "DataBaseSettings.h"

const QString DataBaseSettings::NAME = "iMpos.opt";
const QString DataBaseSettings::HOSTNAME = "iMpos";
IscanderChe
  • 23. Juli 2019 05:28

В репозиторий могу сегодня вечером выложить.

"Или ее надо компилить самому под дистриб?" Тут я не совсем понимаю, что вы имеете ввиду. Я выложу в репозиторий исходный код утилиты, и всё.

IscanderChe
  • 23. Juli 2019 05:33

"Не потребует каждый раз объявлять extern в других файлах". И так не требует. У меня в тестовом классе эти переменные используются без дополнительного объявления. Так же объявил их в cpp-файле один раз, и всё.

Evgenii Legotckoi
  • 23. Juli 2019 05:42

Хорошо, хотя конечно это С, а не С++ ))))

Но если вдруг будут проблемы, то решение через класс со статическими переменными вы видели ))

IscanderChe
  • 23. Juli 2019 12:14

Kommentare

Nur autorisierte Benutzer können Kommentare posten.
Bitte Anmelden oder Registrieren
Letzte Kommentare
ИМ
Игорь Максимов5. Oktober 2024 07:51
Django – Lektion 064. So schreiben Sie eine Python-Markdown-Erweiterung Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55. Juli 2024 11:02
QML - Lektion 016. SQLite-Datenbank und das Arbeiten damit in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssr8. Februar 2024 18:43
Qt Linux - Lektion 001. Autorun Qt-Anwendung unter Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lektion 007. Arbeiten mit ICMP-Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25. Dezember 2023 10:30
Boost - statisches Verknüpfen im CMake-Projekt unter Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
Jetzt im Forum diskutieren
J
JacobFib17. Oktober 2024 03:27
добавить qlineseries в функции Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты https://topdecorpro.ru…
JW
Jhon Wick1. Oktober 2024 15:52
Indian Food Restaurant In Columbus OH| Layla’s Kitchen Indian Restaurant If you're looking for a truly authentic https://www.laylaskitchenrestaurantohio.com/ , Layla’s Kitchen Indian Restaurant is your go-to destination. Located at 6152 Cleveland Ave, Colu…
КГ
Кирилл Гусарев27. September 2024 09:09
Не запускается программа на Qt: точка входа в процедуру не найдена в библиотеке DLL Написал программу на C++ Qt в Qt Creator, сбилдил Release с помощью MinGW 64-bit, бинарнику напихал dll-ки с помощью windeployqt.exe. При попытке запуска моей сбилженной программы выдаёт три оши…
F
Fynjy22. Juli 2024 04:15
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …

Folgen Sie uns in sozialen Netzwerken