IscanderChe
23 июля 2019 г. 13:17

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

Содержание

Базу данных я буду реализовывать на SQLite. В базе должно быть размещено две таблицы: для проектов и для задач. Таблицы в базе данных независимы друг от друга. Все переменные для базы данных и таблиц сделаны глобальными для единообразия.


  1. // database.h
  2.  
  3. #ifndef DATABASE_H
  4. #define DATABASE_H
  5.  
  6. #include <QSql>
  7. #include <QSqlQuery>
  8. #include <QSqlError>
  9. #include <QSqlDatabase>
  10. #include <QDebug>
  11. #include <QApplication>
  12.  
  13. // Настройки базы данных
  14. extern const QString typeDB;
  15. extern const QString hostNameDB;
  16. extern const QString nameDB;
  17.  
  18. // Настройки таблицы проектов
  19. extern const QString projectTable;
  20.  
  21. extern const QString idProjectCol;
  22. extern const QString visibleNameProjectCol;
  23. extern const QString manualPropertyCol;
  24. extern const QString archivePropertyCol;
  25.  
  26. // Настройки таблицы задач
  27. extern const QString taskTable;
  28.  
  29. extern const QString idTaskCol;
  30. extern const QString idTaskProjectCol;
  31. extern const QString openDateTaskCol;
  32. extern const QString typeTaskCol;
  33. extern const QString descriptionTaskCol;
  34. extern const QString statusTaskCol;
  35. extern const QString closeDateTaskCol;
  36. extern const QString revisionTaskCol;
  37.  
  38. // 0 - с поддержкой СКВ, 1 - без поддержки СКВ
  39. enum ProjectVCS
  40. {
  41. WithVCS /* = 0 */,
  42. WithoutVCS /* = 1 */
  43. };
  44.  
  45. // 0 - активный проект, 1 - проект в архиве
  46. enum ProjectArchive
  47. {
  48. Active /* = 0 */,
  49. Archive /* = 1*/
  50. };
  51.  
  52. class DataBase
  53. {
  54. public:
  55. DataBase();
  56. ~DataBase();
  57.  
  58. // Метод добавления новой задачи в базу данных
  59. bool insertNewTask(const int projectId,
  60. const QString& type, const QString& description);
  61.  
  62. // Метод редактирования задачи из базы данных
  63. bool editTask(int id, const QString& type, const QString& description);
  64.  
  65. // Метод определения существования задачи с соответствующим ID
  66. bool isExists(int id);
  67.  
  68. // Метод удаления задачи из базы данных
  69. bool deleteTask(int id);
  70.  
  71. // Метод обновления состояния задачи
  72. bool updateStatusTask(int id, const QString& status);
  73.  
  74. // Метод закрытия задачи
  75. bool closeTask(int id, const QString& revision);
  76.  
  77. // Метод проверки, закрыты ли все задачи в проекте
  78. bool isClosedAllTasks(int projectId);
  79.  
  80. // Метод добавления нового проекта в базу данных
  81. bool insertNewProject(const QString& nameProject, int manual);
  82.  
  83. // Метод определения проекта с поддержкой или без поддержки СКВ
  84. int isManual(int id);
  85.  
  86. // Метод определения, в архиве проект или нет
  87. int isArchive(int id);
  88.  
  89. // Метод архивирования проекта
  90. bool archiveProject(int id);
  91.  
  92. // Метод извлечения проекта из архива
  93. bool extractProject(int id);
  94.  
  95. private:
  96. QSqlDatabase db;
  97. };
  98.  
  99. #endif // DATABASE_H

Теперь рассмотрим реализации приведённых методов.

Начнём с инициализации базы данных.

  1. // database.cpp
  2.  
  3. #include "database.h"
  4. #include <QDate>
  5. #include <QStringList>
  6.  
  7. // Настройки базы данных
  8. const QString typeDB = "QSQLITE";
  9. const QString hostNameDB = "localhost";
  10. const QString nameDB = "db.db";
  11.  
  12. // Настройки таблицы проектов
  13. const QString projectTable = "project_tbl";
  14.  
  15. const QString idProjectCol = "id_project";
  16. const QString visibleNameProjectCol = "name_project";
  17. const QString manualPropertyCol = "manual";
  18. const QString archivePropertyCol = "archive";
  19.  
  20. // Настройки таблицы задач
  21. const QString taskTable = "task_tbl";
  22.  
  23. const QString idTaskCol = "id_task";
  24. const QString idTaskProjectCol = "id_task_project";
  25. const QString openDateTaskCol = "open_date";
  26. const QString typeTaskCol = "type";
  27. const QString descriptionTaskCol = "description";
  28. const QString statusTaskCol = "status";
  29. const QString closeDateTaskCol = "close_date";
  30. const QString revisionTaskCol = "revision";
  31.  
  32. DataBase::DataBase()
  33. {
  34. // Создаём базу данных в формате SQLite
  35. db = QSqlDatabase::addDatabase(typeDB);
  36.  
  37. // Задаём имя сервера базы данных
  38. db.setHostName(hostNameDB);
  39.  
  40. // Размещаем файл базы данных в папке с приложением, чтобы не искать
  41. db.setDatabaseName(qApp->applicationDirPath() + "/" + nameDB);
  42.  
  43. // Если база данных успешно открыта
  44. if(db.open())
  45. qDebug() << "DB open";
  46. // Если что-то пошло не так
  47. else
  48. qDebug() << "DB not open";
  49.  
  50. // Формируем SQL-запрос на создание таблицы проектов
  51. QSqlQuery queryTable;
  52. if(!queryTable.exec(
  53. "CREATE TABLE " + projectTable + " (" +
  54. idProjectCol + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
  55. visibleNameProjectCol + " TEXT, " +
  56. manualPropertyCol + " INTEGER, " +
  57. archivePropertyCol + " INTEGER"
  58. ")"))
  59. // Если запрос выше не может быть выполнен
  60. {
  61. qDebug() << "Error create table " + projectTable;
  62. qDebug() << queryTable.lastError().text();
  63. }
  64. // Если всё прошло успешно
  65. else
  66. {
  67. qDebug() << "Created table " + projectTable;
  68. }
  69.  
  70. // Формируем SQL-запрос на создание таблицы задач
  71. if(!queryTable.exec(
  72. "CREATE TABLE " + taskTable + " (" +
  73. idTaskCol + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
  74. idTaskProjectCol + " INTEGER, " +
  75. openDateTaskCol + " DATE, " +
  76. typeTaskCol + " TEXT, " +
  77. descriptionTaskCol + " TEXT, " +
  78. statusTaskCol + " TEXT, " +
  79. closeDateTaskCol + " DATE, " +
  80. revisionTaskCol + " TEXT"
  81. ")"))
  82. // Если запрос выше не может быть выполнен
  83. {
  84. qDebug() << "Error create table " + taskTable;
  85. qDebug() << queryTable.lastError().text();
  86. }
  87. // Если всё прошло успешно
  88. else
  89. {
  90. qDebug() << "Created table " + taskTable;
  91. }
  92. }

Теперь рассмотрим подробно один из методов базы данных.

  1. // Метод добавления новой задачи в базу данных
  2. bool DataBase::insertNewTask(const int projectId,
  3. const QString& type, const QString& description)
  4. {
  5. // Формируем SQL-запрос на вставку данных
  6. QSqlQuery queryInsert;
  7. queryInsert.prepare(
  8. "INSERT INTO " + taskTable +
  9. " (" +
  10. idTaskProjectCol + ", " +
  11. openDateTaskCol + ", " +
  12. typeTaskCol + ", " +
  13. descriptionTaskCol + ", " +
  14. statusTaskCol +
  15. ") "
  16. "VALUES (:ProjectId, :Date, :Type, :Description, :Status)"
  17. );
  18. queryInsert.bindValue(":ProjectId", projectId);
  19. queryInsert.bindValue(":Date", QDate::currentDate());
  20. queryInsert.bindValue(":Type", type);
  21. queryInsert.bindValue(":Description", description);
  22. queryInsert.bindValue(":Status", "не активна");
  23.  
  24. // Если запрос не может быть выполнен
  25. if(!queryInsert.exec())
  26. {
  27. qDebug() << "Error insert data into " + taskTable;
  28. qDebug() << queryInsert.lastError().text();
  29. return false;
  30. }
  31. // Если всё прошло успешно
  32. else
  33. {
  34. qDebug() << "Inserted data into " + taskTable;
  35. return true;
  36. }
  37.  
  38. return false;
  39. }

Остальные методы написаны по образу и подобию, поэтому их код я опущу.

Переходим к тестированию написанных методов.

Перед тем как начать писать код тестов, положим в папку с кодом проекта файл DataBase.pri, чтобы было удобнее подключать класс DataBase в тесты.

  1. # DataBase.pri
  2.  
  3. SOURCES += \
  4. $$PWD/database.cpp
  5.  
  6. HEADERS += \
  7. $$PWD/database.h

Создаём подпроект Test_DataBase и одноимённый класс для тестирования, а также подключаем common.pri.

В Qt Creator есть возможность автоматизировано создавать тесты, но я ею не пользуюсь, поскольку запустить тест можно только из самого Qt Creator. В моём случае получается отдельный исполняемый файл, запустив который с соответствующими параметрами командной строки, можно получить отчёт о тестировании в виде xml-файла.

  1. # Test_DataBase.pro
  2.  
  3. include(../../common.pri)
  4. include(../../ICTrackerServer/DataBase.pri)
  5.  
  6. QT += core gui testlib sql
  7.  
  8. CONFIG += c++11
  9.  
  10. greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
  11.  
  12. TARGET = Test_DataBase
  13. TEMPLATE = app
  14.  
  15. DEFINES += QT_DEPRECATED_WARNINGS
  16.  
  17. SOURCES += \
  18. main.cpp \
  19. test_database.cpp
  20.  
  21. HEADERS += \
  22. test_database.h
  1. // main.cpp
  2.  
  3. #include "test_database.h"
  4. #include <QApplication>
  5. #include <QTest>
  6.  
  7. int main(int argc, char* argv[])
  8. {
  9. QCoreApplication a(argc, argv);
  10. QTest::qExec(new Test_DataBase(), argc, argv);
  11. return 0;
  12. }
  13.  
  1. // test_database.h
  2.  
  3. #ifndef TEST_DATABASE_H
  4. #define TEST_DATABASE_H
  5.  
  6. #include "../../ICTrackerServer/database.h"
  7. #include <QObject>
  8. #include <QTest>
  9.  
  10. class Test_DataBase : public QObject
  11. {
  12. Q_OBJECT
  13.  
  14. public:
  15. explicit Test_DataBase(QObject* parent = nullptr);
  16.  
  17. private:
  18. // Объявляем указатель на объект базы данных
  19. DataBase* db;
  20.  
  21. private slots:
  22. // Тестирование метода bool insertNewTask(const QDate& openDate, const QString& type,
  23. // const QString& description)
  24. void insertNewTask();
  25.  
  26. // Тестирование метода bool editTask(int id, const QString& type, const QString& description)
  27. void editTask();
  28.  
  29. // Тестирование метода bool isExists(int id)
  30. void isExists();
  31.  
  32. // Тестирование метода bool deleteTask(int id)
  33. void deleteTask();
  34.  
  35. // Тестирование метода bool updateStatusTask(int id, const QString& status)
  36. void updateStatusTask();
  37.  
  38. // Тестирование метода bool closeTask(int id, const QDate& closeDate, const QString& revision)
  39. void closeTask();
  40.  
  41. // Тестирование метода bool insertNewProject(const QString& nameProject, int manual)
  42. void insertNewProject();
  43.  
  44. // Тестирование метода int isManual(int id)
  45. void isManual();
  46.  
  47. // Тестирование метода int isArchive(int id)
  48. void isArchive();
  49.  
  50. // Тестирование метода bool archiveProject(int id)
  51. void archiveProject();
  52.  
  53. // Тестирование метода bool extractProject(int id)
  54. void extractProject();
  55. };
  56.  
  57. #endif // TEST_DATABASE_H

Рассмотрим подробно только первый слот тестирования. Остальные более-менее аналогичны ему.

  1. // test_database.cpp
  2.  
  3. #include "test_database.h"
  4. #include <QDate>
  5. #include <QDebug>
  6.  
  7. Test_DataBase::Test_DataBase(QObject* parent) : QObject(parent)
  8. {
  9. // Создаём новый объект DataBase. Помним, что создаётся база данных и таблицы.
  10. db = new DataBase;
  11. }
  12.  
  13. void Test_DataBase::insertNewTask()
  14. {
  15. // Формируем данные для тестирования
  16. int id = 1;
  17. int projectId = 1;
  18. QDate date = QDate::currentDate();
  19. QString type = "bug";
  20. QString description = "Description 1";
  21. QString status = "не активна";
  22. QDate closeDate = QDate(0, 0, 0);
  23. QString revision = "";
  24.  
  25. // Для удобства сравнения помещаем данные в список QVariant
  26. QVariantList query;
  27. query << id << projectId << date << type << description << status
  28. << closeDate << revision;
  29.  
  30. // Добавляем данные в базу данных
  31. db->insertNewTask(projectId, type, description);
  32.  
  33. // Создаём список для результатов запроса к базе данных
  34. QVariantList result;
  35.  
  36. // Формируем запрос к базе данных
  37. QSqlQuery querySelect;
  38. querySelect.prepare(
  39. "SELECT * FROM " + taskTable + " WHERE " + idTaskCol + " = :Id");
  40. querySelect.bindValue(":Id", id);
  41.  
  42. // Если запрос не может быть выполнен
  43. if(!querySelect.exec())
  44. {
  45. qDebug() << "Test_DataBase::insertNewTask() Error select from table " + taskTable;
  46. qDebug() << querySelect.lastError().text();
  47. }
  48. // Если всё прошло успешно
  49. else
  50. {
  51. while(querySelect.next())
  52. {
  53. // Заполняем список результатами запроса
  54. result << querySelect.value(0).toInt();
  55. result << querySelect.value(1).toInt();
  56. result << querySelect.value(2).toDate();
  57. result << querySelect.value(3).toString();
  58. result << querySelect.value(4).toString();
  59. result << querySelect.value(5).toString();
  60. result << querySelect.value(6).toDate();
  61. result << querySelect.value(7).toString();
  62. }
  63. }
  64.  
  65. // Сравниваем исходные данные и результат.
  66. QCOMPARE(query, result);
  67.  
  68. // Исход для теста должен быть PASS.
  69. // Если исход FAIL - внимательно читать вывод теста, там содержится вся информация об ошибке.
  70. // И помнить главное: ошибка может быть в основном коде, а может - в коде теста...
  71. }

На выходе тестирования получаем:

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

Собственно, что и требовалось. Стоит отметить, что в passed входят два дефолтных слота, которые выполняются в начале и в конце тестирования методов: это initTestCase() и cleanupTestCase() соответственно.

Для красоты выгрузим данные в xml-файл.

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

Открою его с помощью самостоятельно разработанной утилиты TRViewer.

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

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

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

По статье задано0вопрос(ов)

4

Вам это нравится? Поделитесь в социальных сетях!

BlinCT
  • 23 июля 2019 г. 14:32

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

Evgenii Legotckoi
  • 23 июля 2019 г. 14:32

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

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

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

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

Header

  1. class DataBaseSettings
  2. {
  3. public:
  4. static const QString NAME;
  5. static const QString HOSTNAME;
  6. }

CPP

  1. #include "DataBaseSettings.h"
  2.  
  3. const QString DataBaseSettings::NAME = "iMpos.opt";
  4. const QString DataBaseSettings::HOSTNAME = "iMpos";
IscanderChe
  • 23 июля 2019 г. 15:28

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

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

IscanderChe
  • 23 июля 2019 г. 15:33

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

Evgenii Legotckoi
  • 23 июля 2019 г. 15:42

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

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

IscanderChe
  • 23 июля 2019 г. 22:14

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
  • Последние комментарии
  • IscanderChe
    12 апреля 2025 г. 17:12
    Добрый день. Спасибо Вам за этот проект и отдельно за ответы на форуме, которые мне очень помогли в некоммерческих пет-проектах. Профессиональным программистом я так и не стал, но узнал мно…
  • AK
    1 апреля 2025 г. 11:41
    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
  • Evgenii Legotckoi
    9 марта 2025 г. 21:02
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    9 марта 2025 г. 16:14
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…
  • ИМ
    22 ноября 2024 г. 21:51
    Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…