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
OI

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

  • Result:40points,
  • Rating points-8
AD

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

  • Result:50points,
  • Rating points-4
m

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

  • Result:80points,
  • Rating points4
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 11:51 a.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiOct. 31, 2024, 2:37 p.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 8:19 a.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 7:51 a.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 11:02 a.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
Evgenii Legotckoi
Evgenii LegotckoiJune 24, 2024, 3:11 p.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 6:04 a.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 3:49 a.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimOct. 25, 2024, 9:10 a.m.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Follow us in social networks