Проект Simple Tracker. Часть 3: сервер. База данных и её тестирование

Simple Tracker, Qt, Iscander Che, C++

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

Работа над базой данных закончена.

Вместо заключения

По-хорошему, нужно делать наоборот – вначале писать тесты, потом сами методы. Я вначале писал методы, потом тесты. И немного за это поплатился: пришлось разработать несколько методов, изначально не планировавшихся. Но писать тесты там, где это только возможно – необходимо. Практический опыт этого проекта показал, что при невнимательном добавлении нового поля в базу данных она может поломаться в любом месте. Найти проблемное место без тестирования было бы гораздо труднее.

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.

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

Есть комментарий по вашему коду. Лучше бы вместо глобальных переменных в стиле Си, то есть с использоавнием 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";

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

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

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

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

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

Comments

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

Hello, Dear Users of EVILEG!!!

If the site helped you, then support the development of the site financially, please.

You can do it by following ways:

Thank you, Evgenii Legotckoi

n
Dec. 9, 2019, 12:46 p.m.
nastya4554

C++ - Тест 003. Условия и циклы

  • Result:78points,
  • Rating points2
N
Dec. 9, 2019, 4:49 a.m.
Nnnnanananna

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

  • Result:20points,
  • Rating points-10
NB
Dec. 9, 2019, 3:29 a.m.
Nikolaj Batmanov

C++ - Test 002. Constants

  • Result:58points,
  • Rating points-2
Last comments
Dec. 9, 2019, 3:41 a.m.
Evgenij Legotskoj

Эта ошибка invalid use of incomplete type ‘class Ui::AnotherWindow’ обычно говорит о том, что не найдено определение класса или структуры. Типичная проблема - не подключён заголовочны…
NB
Dec. 9, 2019, 3:36 a.m.
Nikolaj Batmanov

Ну, не настолько со мной всё полхо...))) Вроде бы. Я ж кнопки отрисовываю.
Dec. 9, 2019, 3:14 a.m.
Evgenij Legotskoj

Добрый день. У вас ui файлов по ходу нет. UI файлы используются для вёрстки в графическом дизайнере.
NB
Dec. 9, 2019, 3:05 a.m.
Nikolaj Batmanov

Здравствуйте! Полностью скопировал ваш пример к себе, чтобы разобраться. А он не хочет запускаться, дает ошибку: invalid use of incomplete type ‘class Ui::AnotherWindow’ ui(new Ui…
Dec. 8, 2019, 7:23 a.m.
Evgenij Legotskoj

У меня здесь есть одна старая статья с примером векторного редактора. Там есть ответы на ваши вопросы. Поизучайте Qt/C++ - Урок 072. Пример векторного редактора на Qt QGraphicsItem, QG…
Now discuss on the forum
Dec. 9, 2019, 7:16 a.m.
qml_puthon_user

Я сделал простой вывод текста по испусканию сигнала... Чего не хватает программе?) Python: # системные библиотекиimport cv2import numpy as npimport sysimport threading# PyQt б…
SK
Dec. 8, 2019, 4:11 p.m.
Semen Kosandjak

інтерфейс qt, приклад того додаю на малюнку, я натискаю на кнопку і у мене з'являється 2 текст лайну в які я вводжу значення, тобто в 1 цифри,у другому випадку це літери, тобто c++, без графічно…
Dec. 8, 2019, 10:21 a.m.
qml_puthon_user

Тут не подскажу, пишу на питоне.)
Dec. 8, 2019, 5:31 a.m.
BlinCT

Всем привет. Впервые столкнулся со сборкой buildroot и наткнулся на пару странных ошибок. Надеюсь кто то прочитавший сможет что то подсказать, так как мне пока они не понятный. Пе…
Dec. 5, 2019, 4:12 p.m.
Evgenij Legotskoj

Это уже кастомная стилизация. Придётся отключать обрамление и самостоятельно реализовывать ресайз окна, его перемещение, стиль и т.д. Вот статья, как отключить обрамление окна - QML …
EVILEG
About
Services
© EVILEG 2015-2019
Recommend hosting TIMEWEB