IscanderChe
July 23, 2019, 1:17 p.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.


  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

Now consider the implementation of the above methods.

Let's start by initializing the database.

  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. }

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

  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. }

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.

  1. # DataBase.pri
  2.  
  3. SOURCES += \
  4. $$PWD/database.cpp
  5.  
  6. HEADERS += \
  7. $$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.

  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

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

  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. }

As a result of testing, we get:

  1. Totals: 13 passed, 0 failed, 0 skipped, 0 blacklisted, 1113ms
  2. ********* 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.

  1. 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.

By article asked0question(s)

4

Do you like it? Share on social networks!

BlinCT
  • July 23, 2019, 2:32 p.m.

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

Evgenii Legotckoi
  • July 23, 2019, 2:32 p.m.

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

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

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

IscanderChe
  • July 23, 2019, 3:33 p.m.

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

Evgenii Legotckoi
  • July 23, 2019, 3:42 p.m.

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

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

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

Comments

Only authorized users can post comments.
Please, Log in or Sign up
  • Last comments
  • Evgenii Legotckoi
    March 9, 2025, 9:02 p.m.
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    March 9, 2025, 4:14 p.m.
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…
  • ИМ
    Nov. 22, 2024, 9:51 p.m.
    Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
  • Evgenii Legotckoi
    Oct. 31, 2024, 11:37 p.m.
    Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
  • A
    Oct. 19, 2024, 5:19 p.m.
    Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html