IscanderChe
IscanderCheШілде 23, 2019, 3:17 Т.Ж.

Қарапайым трекер жобасы. 3-бөлім: сервер. Мәліметтер қоры және оны тестілеу

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

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

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

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

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Ол саған ұнайды ма? Әлеуметтік желілерде бөлісіңіз!

BlinCT
  • Шілде 23, 2019, 4:32 Т.Ж.

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

Evgenii Legotckoi
  • Шілде 23, 2019, 4: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, 2019, 5:28 Т.Ж.

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

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

IscanderChe
  • Шілде 23, 2019, 5:33 Т.Ж.

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

Evgenii Legotckoi
  • Шілде 23, 2019, 5:42 Т.Ж.

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

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

IscanderChe
  • Шілде 23, 2019, 12:14 Т.Қ.

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз
OI
  • Ora Iro
  • Жел. 24, 2024, 6:38 Т.Ж.

C++ - Тест 001. Первая программа и типы данных

  • Нәтиже:40ұпай,
  • Бағалау ұпайлары-8
AD

C++ - Тест 004. Указатели, Массивы и Циклы

  • Нәтиже:50ұпай,
  • Бағалау ұпайлары-4
m
  • molni99
  • Қаз. 26, 2024, 1:37 Т.Ж.

C++ - Тест 004. Указатели, Массивы и Циклы

  • Нәтиже:80ұпай,
  • Бағалау ұпайлары4
Соңғы пікірлер
ИМ
Игорь МаксимовҚар. 22, 2024, 11:51 Т.Ж.
Django - Оқулық 017. Теңшелген Django кіру беті Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiҚаз. 31, 2024, 2:37 Т.Қ.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEҚаз. 19, 2024, 8:19 Т.Ж.
Qt Creator көмегімен fb3 файл оқу құралы Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовҚаз. 5, 2024, 7:51 Т.Ж.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5Шілде 5, 2024, 11:02 Т.Ж.
QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Енді форумда талқылаңыз
Evgenii Legotckoi
Evgenii LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Қар. 15, 2024, 6:04 Т.Ж.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectМаусым 4, 2022, 3:49 Т.Ж.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimҚаз. 25, 2024, 9:10 Т.Ж.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Бізді әлеуметтік желілерде бақылаңыз