IscanderChe
IscanderCheJuly 23, 2019, 3:17 a.m.

Simple Tracker project. Part 3: server. Database and its testing

I will implement the database in SQLite. The database should contain two tables: for projects and for tasks. The tables in a database are independent of each other. All database and table variables are global for consistency.


// 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

Now consider the implementation of the above methods.

Let's start by initializing the database.

// 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;
    }
}

Now let's take a closer look at one of the database methods.

// Метод добавления новой задачи в базу данных
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;
}

The remaining methods are written in the image and likeness, so I will omit their code.

Let's move on to testing the written methods.

Before we start writing the test code, let's put the DataBase.pri file in the folder with the project code to make it easier to include the DataBase class in tests.

# DataBase.pri

SOURCES += \
        $$PWD/database.cpp

HEADERS += \
        $$PWD/database.h

We create a subproject Test_DataBase and a class of the same name for testing, and also include common.pri.

Qt Creator has the ability to automatically create tests, but I do not use it, since you can only run a test from Qt Creator itself. In my case, a separate executable file is obtained, by running it with the appropriate command line parameters, you can get a testing report in the form of an xml file.

# 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

Let us consider in detail only the first testing slot. The rest are more or less the same.

// 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 - внимательно читать вывод теста, там содержится вся информация об ошибке.
    // И помнить главное: ошибка может быть в основном коде, а может - в коде теста...
}

As a result of testing, we get:

Totals: 13 passed, 0 failed, 0 skipped, 0 blacklisted, 1113ms
********* Finished testing of Test_DataBase *********

Actually, what was required. It is worth noting that pass includes two default slots that are executed at the beginning and end of method testing: these are initTestCase() and cleanupTestCase(), respectively.

For beauty, let's unload the data into an xml file.

Test_DataBase.exe –xml –o <путь к папке с отчётами>\report.xml

I will open it with the help of a self-developed TRViewer utility.

The database has been completed.

Instead of conclusion

In a good way, you need to do the opposite - first write tests, then the methods themselves. I first wrote methods, then tests. And I paid a little for it: I had to develop several methods that were not originally planned. But writing tests wherever possible is necessary. The practical experience of this project showed that if a new field is added to the database carelessly, it can break anywhere. Finding the problem area without testing would be much more difficult.

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

BlinCT
  • July 23, 2019, 4:32 a.m.

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

Evgenii Legotckoi
  • July 23, 2019, 4:32 a.m.

Есть комментарий по вашему коду. Лучше бы вместо глобальных переменных в стиле Си, то есть с использоавнием 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
  • July 23, 2019, 5:28 a.m.

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

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

IscanderChe
  • July 23, 2019, 5:33 a.m.

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

Evgenii Legotckoi
  • July 23, 2019, 5:42 a.m.

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

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

IscanderChe
  • July 23, 2019, 12:14 p.m.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
SH

C++ - Test 001. The first program and data types

  • Result:33points,
  • Rating points-10
г
  • ги
  • April 24, 2024, 1:51 a.m.

C++ - Test 005. Structures and Classes

  • Result:41points,
  • Rating points-8
l
  • laei
  • April 23, 2024, 7:19 p.m.

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:10points,
  • Rating points-10
Last comments
k
kmssrFeb. 9, 2024, 5:43 a.m.
Qt Linux - Lesson 001. Autorun Qt application under Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lesson 007. Working with ICMP Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVADec. 25, 2023, 9:30 p.m.
Boost - static linking in CMake project under Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJoDec. 25, 2023, 7:38 p.m.
Boost - static linking in CMake project under Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
GvozdikDec. 19, 2023, 8:01 a.m.
Qt/C++ - Lesson 056. Connecting the Boost library in Qt for MinGW and MSVC compilers Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Now discuss on the forum
G
GarApril 22, 2024, 3:46 p.m.
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil AcademicsApril 20, 2024, 5:45 p.m.
Unlock Your Aesthetic Potential: Explore MSC in Facial Aesthetics and Cosmetology in India Embark on a transformative journey with an msc in facial aesthetics and cosmetology in india . Delve into the intricate world of beauty and rejuvenation, guided by expert faculty and …
a
a_vlasovApril 14, 2024, 4:41 p.m.
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел ДорофеевApril 14, 2024, 12:35 p.m.
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrexApril 4, 2024, 2:47 p.m.
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…

Follow us in social networks