with one click
with one click
QtPass CI/CD workflow - run GitHub Actions locally with act, linters, formatters
QtPass localization workflow - translation files, updating, adding languages
QtPass localization audit - structural checks on .ts files (placeholders, HTML balance, mnemonics, mixed-script artifacts)
QtPass GitHub interaction - PRs, issues, branches, merging
Bug fixing workflow for QtPass - find, fix, test, PR
Documentation guide for QtPass - README, FAQ, localization
| name | qtpass-testing |
| description | Comprehensive guide for QtPass unit testing with Qt Test |
| license | GPL-3.0-or-later |
| metadata | {"audience":"developers","workflow":"testing"} |
QtPass is a Qt6/C++ password manager GUI for pass. Tests use Qt Test framework.
# Build and run all tests
make check
# Build with coverage
qmake6 -r CONFIG+=coverage
make -j4
# Generate coverage report
make lcov
Tests for src/gpgkeystate.cpp:
parseMultiKeyPublic() - Multiple public keys parsingparseSecretKeys() - Secret key detection (have_secret flag)parseSingleKey() - Single key with and without fingerprintparseKeyRollover() - Multiple keys in sequenceclassifyRecordTypes() - GPG record type classification (pub, sec, uid, fpr, etc.)Store sample test data in tests/fixtures/:
ls tests/fixtures/
# gpg-colons-multi-key.txt
# gpg-colons-public.txt
# gpg-colons-secret.txt
These contain real GPG --with-colons output for deterministic testing.
Tests for src/util.cpp:
normalizeFolderPath() - Path normalizationfileContent() / fileContentEdgeCases() - FileContent parsingregexPatterns() / regexPatternEdgeCases() - URL detection regular expressiontotpHiddenFromDisplay() - OTP field hidinguserInfoValidity() - User key validationpasswordConfigurationCharacters() - Password character setssimpleTransaction*() - SimpleTransaction testsTests for src/filecontent.h:
parsePlainPassword() - Single-line passwordparsePasswordWithNamedFields() - Password with key:valueparseWithTemplateFields() - Template field parsingparseWithAllFields() - All fields modegetRemainingData() - Non-template fieldsgetRemainingDataForDisplay() - Hides otpauth://namedValuesTakeValue() / namedValuesTakeValueNotFound()Tests for src/passwordconfiguration.h:
passwordConfigurationDefaults() - Default valuespasswordConfigurationSetters() - Setter methodspasswordConfigurationCharacterSets() - Character set configTests for src/executor.h:
executeBlockingEcho() - Basic executionexecuteBlockingWithArgs() - Arguments handlingexecuteBlockingExitCode() - Exit code checking (Unix only)executeBlockingStderr() - Error output capture (Unix only)Tests for src/storemodel.h:
dataRemovesGpgExtension() - Display name filteringflagsWithValidIndex() / flagsWithInvalidIndex() - Item flagsmimeTypes() - Drag/drop MIME typeslessThan() - Sorting comparisonsupportedDropActions() / supportedDragActions()filterAcceptsRowHidden() / filterAcceptsRowVisible()Tests for src/qtpasssettings.h:
UI tests:
contentRemainsSame() - Password content integrityemptyPassword() - Empty password handlingmultilineRemainingData() - Multiline field handling// SPDX-FileCopyrightText: YYYY Your Name
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QtTest>
#include "../../../src/mymodule.h"
class tst_mymodule : public QObject {
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void testBasicFunction();
void testEdgeCase();
void cleanupTestCase();
};
void tst_mymodule::initTestCase() {}
void tst_mymodule::testBasicFunction() {
// Use set+get pattern or direct input/output
QString result = MyModule::process("input");
QVERIFY2(result == "expected", "Should return expected output");
}
void tst_mymodule::testEdgeCase() {
// Test empty, null, boundary conditions
QVERIFY(MyModule::process("").isEmpty());
}
void tst_mymodule::cleanupTestCase() {}
QTEST_MAIN(tst_mymodule)
#include "tst_mymodule.moc"
!include(../auto.pri) { error("Couldn't find the auto.pri file!") }
SOURCES += tst_mymodule.cpp
LIBS = -L"$$OUT_PWD/../../../src/$(OBJECTS_DIR)" -lqtpass $$LIBS
clang|gcc:PRE_TARGETDEPS += "$$OUT_PWD/../../../src/$(OBJECTS_DIR)/libqtpass.a"
HEADERS += mymodule.h
OBJ_PATH += ../../../src/$(OBJECTS_DIR)
VPATH += ../../../src
INCLUDEPATH += ../../../src
win32 {
RC_FILE = ../../../windows.rc
QMAKE_LINK_OBJECT_MAX=24
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>QMTestSpecification</key>
<dict>
<key>Type</key>
<string>Bundle</string>
<key>UIElement</key>
<dict>
<key>Modified</key>
<false/>
<key>SystemEntity</key>
<string>Test</string>
</dict>
</dict>
</dict>
</plist>
tests/auto/<name>/<name>.pro file (copy pattern above)qtpass.plist (copy from model/)tst_<name>.cpp test filetests/auto/auto.pro: SUBDIRS += <name>qmake6 -r && make -j4make checktst_<classname>.cpptst_<classname>testMethodName() or methodDoesWhat()initTestCase() - Setup (runs once before all tests)init() - Setup before each testcleanupTestCase() - Teardown (runs once after all tests)// Basic
QVERIFY(condition);
QVERIFY2(condition, "failure message");
// Equality
QCOMPARE(actual, expected);
QCOMPARE2(actual, expected, "message");
// String matching
QVERIFY2(output.contains("needle"), "should contain needle");
QCOMPARE(QString("hello").toUpper(), QString("HELLO"));
|| in assertionsBad (tautology or ambiguous):
QVERIFY(result == expected || result == INVALID); // unclear intent
QVERIFY(result != INVALID || result == INVALID); // ALWAYS TRUE - tautology!
Good - just call the method to verify it doesn't crash:
simpleTransaction trans;
trans.transactionAdd(Enums::PASS_INSERT);
trans.transactionIsOver(Enums::PASS_INSERT); // verify it runs without crash
Or use deterministic setup with QCOMPARE:
simpleTransaction st;
st.transactionAdd(Enums::PASS_INSERT);
Enums::PROCESS result = st.transactionIsOver(Enums::PASS_INSERT);
QCOMPARE(result, Enums::PASS_INSERT); // deterministic
QFAIL("message") - Fail with messageQSKIP("message") - Skip testQCOMPARE(a, b) - Assert equalityQVERIFY(a) - Assert trueQVERIFY2(a, msg) - Assert with messagevoid tst_executor::unixOnlyTest() {
#ifndef Q_OS_WIN
// Test code here
#endif
}
Windows uses backslashes (\) while Unix uses forward slashes (/). When comparing paths, use QDir::cleanPath() to normalize:
void tst_util::testPathComparison() {
QString path = Pass::getGpgIdPath(passStore);
QString expected = passStore + "/.gpg-id";
// Use cleanPath to normalize for cross-platform compatibility
QVERIFY2(QDir::cleanPath(path) == QDir::cleanPath(expected),
qPrintable(QString("Expected %1, got %2")
.arg(QDir::cleanPath(expected), QDir::cleanPath(path))));
}
Tests that modify QtPass settings can pollute the user's live config. This is especially problematic on Windows where settings use the registry.
Solution: Backup and restore settings in tests
// In tst_settings::initTestCase()
void tst_settings::initTestCase() {
// Check for portable mode (qtpass.ini in app directory)
QString portable_ini = QCoreApplication::applicationDirPath() +
QDir::separator() + "qtpass.ini";
bool isPortableMode = QFile::exists(portable_ini);
if (isPortableMode) {
// Backup settings file
QtPassSettings::getInstance()->sync();
QString settingsFile = QtPassSettings::getInstance()->fileName();
m_settingsBackupPath = settingsFile + ".bak";
QFile::remove(m_settingsBackupPath);
QFile::copy(settingsFile, m_settingsBackupPath);
} else {
// Warn on non-portable mode (registry on Windows)
qWarning() << "Non-portable mode detected. Tests may modify registry settings.";
}
}
// In tst_settings::cleanupTestCase()
void tst_settings::cleanupTestCase() {
// Restore original settings after all tests
if (isPortableMode && !m_settingsBackupPath.isEmpty()) {
QString settingsFile = QtPassSettings::getInstance()->fileName();
QFile::remove(settingsFile);
QFile::copy(m_settingsBackupPath, settingsFile);
QFile::remove(m_settingsBackupPath);
}
}
Key points:
When checking variant types, prefer canConvert<T>() over metaType().id() for broader compatibility:
// Qt6-only (fails on Qt5)
QVERIFY(displayData.metaType().id() == QMetaType::QString);
// Qt5/Qt6 compatible
QVERIFY(displayData.canConvert<QString>());
void tst_mymodule::testWithTempFile() {
QTemporaryDir tempDir;
QString filePath = tempDir.path() + "/test.txt";
QFile file(filePath);
QVERIFY(file.open(QIODevice::WriteOnly));
file.write("test data");
file.close();
// Test reads/modifies file
}
When testing settings that have getters with default parameters, pass a different default value to verify persistence:
// Bad - returns default if persistence fails
setter(testValue);
QCOMPARE(getter(testValue), testValue);
// Good - uses different default, must return stored value
setter(testValue);
QCOMPARE(getter(!testValue), testValue); // bool: use negation
QCOMPARE(getter(-1), testValue); // int: use sentinel
QCOMPARE(getter(QString()), testValue); // string: use empty
Data-driven tests work well for simple bool/int/string settings. Compound types like PasswordConfiguration often don't fit:
Always use the proper Qt Test macros:
// Good
QCOMPARE(actual, expected);
QVERIFY(condition);
QVERIFY2(condition, "failure message");
// Bad - won't compile or is confusing
COMPARE(actual, expected); // missing Q
QQCOMPARE(actual, expected); // extra Q
normalizeFolderPath() - Path normalizationprotocolRegex() - URL detectionendsWithGpg() - Extension matchingnewLinesRegex() - Newline detectionFileContent::parse() - Parse password filegetPassword() - Get main passwordgetNamedValues() - Get key:value fieldsgetRemainingData() - Non-template fieldsgetRemainingDataForDisplay() - Display-safe (hides OTP)NamedValues::takeValue() - Extract and remove valuePasswordConfiguration classExecutor::executeBlocking() - Run command synchronouslyStoreModel extends QFileSystemModelsetModelAndStore() - Initialize with model and pathdata() - Returns display name (removes .gpg)flags() - Item flagslessThan() - SortingTests run via make check in CI. Coverage reported with make lcov.
See qtpass-linting skill for full CI workflow. Pattern:
# Run linter locally BEFORE pushing
act push -W .github/workflows/linter.yml -j build
Formats: .md, .yml, .html, .css, .js, .json, etc.
npx prettier --write <file>
npx prettier --write .github/workflows/*.yml
npx prettier --write ".opencode/skills/*/SKILL.md"
# Check formatting
clang-format --style=file --dry-run src/main.cpp
# Apply formatting
clang-format --style=file -i src/main.cpp
make check # Runs tests and builds
# Run single test
./tests/auto/util/tst_util testName
# Verbose output
./tests/auto/util/tst_util -v2
# Detailed timing
./tests/auto/util/tst_util -vs