Базу данных я буду реализовывать на 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.
Работа над базой данных закончена.
Вместо заключения
По-хорошему, нужно делать наоборот – вначале писать тесты, потом сами методы. Я вначале писал методы, потом тесты. И немного за это поплатился: пришлось разработать несколько методов, изначально не планировавшихся. Но писать тесты там, где это только возможно – необходимо. Практический опыт этого проекта показал, что при невнимательном добавлении нового поля в базу данных она может поломаться в любом месте. Найти проблемное место без тестирования было бы гораздо труднее.
Отличная статья.
Вопрос по поводу утилиты TRViewer, она в репозитории есть? Или ее надо компилить самому под дистриб?
Есть комментарий по вашему коду. Лучше бы вместо глобальных переменных в стиле Си, то есть с использоавнием extern, написали бы статические переменные в рамках класса. IMHO - это будет выглядеть более лаконично, и если не ошибаюсь, то не потребует каждый раз объявлять extern в других файлах.
По факту, если у вас есть глобальная переменная, то вам придётся в каждом другом файле объявлять её как extern. Если у вас есть 50 таких переменных, которые активно используются, то придёдтся попотеть, чтобы не забыть их объявлять везде, где потребуется.
Проще написать класс, со статическими переменными, и потом просто подключать его заголовочный файл там, где потребуется.
Это будет выглядеть так
Header
CPP
В репозиторий могу сегодня вечером выложить.
"Или ее надо компилить самому под дистриб?" Тут я не совсем понимаю, что вы имеете ввиду. Я выложу в репозиторий исходный код утилиты, и всё.
"Не потребует каждый раз объявлять extern в других файлах". И так не требует. У меня в тестовом классе эти переменные используются без дополнительного объявления. Так же объявил их в cpp-файле один раз, и всё.
Хорошо, хотя конечно это С, а не С++ ))))
Но если вдруг будут проблемы, то решение через класс со статическими переменными вы видели ))
Вот ссылка: https://github.com/iscander-che/TestReportViewer .