| 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 Testing Guide
Project Overview
QtPass is a Qt6/C++ password manager GUI for pass. Tests use Qt Test framework.
Quick Start
make check
qmake6 -r CONFIG+=coverage
make -j4
make lcov
Existing Test Suites
The suites below are representative, not exhaustive — tests/auto/auto.pro
SUBDIRS is the source of truth. Newer suites include settings,
configdialog, mainwindow, importkeydialog, exportpublickeydialog,
keygendialog, trayicon, userinfo, profileinit, grepsearchcontroller
and passworddisplaypanel.
tests/auto/gpgkeystate/tst_gpgkeystate.cpp
Tests for src/gpgkeystate.cpp:
parseMultiKeyPublic() - Multiple public keys parsing
parseSecretKeys() - Secret key detection (have_secret flag)
parseSingleKey() - Single key with and without fingerprint
parseKeyRollover() - Multiple keys in sequence
classifyRecordTypes() - GPG record type classification (pub, sec, uid, fpr, etc.)
Test Fixtures
Store sample test data in tests/fixtures/:
ls tests/fixtures/
These contain real GPG --with-colons output for deterministic testing.
tests/auto/util/tst_util.cpp
Tests for src/util.cpp:
normalizeFolderPath() - Path normalization
fileContent() / fileContentEdgeCases() - FileContent parsing
regexPatterns() / regexPatternEdgeCases() - URL detection regular expression
totpHiddenFromDisplay() - OTP field hiding
userInfoValidity() - User key validation
passwordConfigurationCharacters() - Password character sets
simpleTransaction*() - SimpleTransaction tests
tests/auto/filecontent/tst_filecontent.cpp
Tests for src/filecontent.h:
parsePlainPassword() - Single-line password
parsePasswordWithNamedFields() - Password with key:value
parseWithTemplateFields() - Template field parsing
parseWithAllFields() - All fields mode
getRemainingData() - Non-template fields
getRemainingDataForDisplay() - Hides otpauth://
namedValuesTakeValue() / namedValuesTakeValueNotFound()
tests/auto/passwordconfig/tst_passwordconfig.cpp
Tests for src/passwordconfiguration.h:
passwordConfigurationDefaults() - Default values
passwordConfigurationSetters() - Setter methods
passwordConfigurationCharacterSets() - Character set config
tests/auto/executor/tst_executor.cpp
Tests for src/executor.h:
executeBlockingEcho() - Basic execution
executeBlockingWithArgs() - Arguments handling
executeBlockingExitCode() - Exit code checking (Unix only)
executeBlockingStderr() - Error output capture (Unix only)
tests/auto/model/tst_storemodel.cpp
Tests for src/storemodel.h:
dataRemovesGpgExtension() - Display name filtering
flagsWithValidIndex() / flagsWithInvalidIndex() - Item flags
mimeTypes() - Drag/drop MIME types
lessThan() - Sorting comparison
supportedDropActions() / supportedDragActions()
filterAcceptsRowHidden() / filterAcceptsRowVisible()
tests/auto/settings/tst_settings.cpp
Tests for src/qtpasssettings.h:
- Uses set+get pattern for each setting
- Tests isUseGit(), setUseGit(), etc.
tests/auto/ui/tst_ui.cpp
UI tests:
contentRemainsSame() - Password content integrity
emptyPassword() - Empty password handling
multilineRemainingData() - Multiline field handling
Test File Template
#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() {
QString result = MyModule::process("input");
QVERIFY2(result == "expected", "Should return expected output");
}
void tst_mymodule::testEdgeCase() {
QVERIFY(MyModule::process("").isEmpty());
}
void tst_mymodule::cleanupTestCase() {}
QTEST_MAIN(tst_mymodule)
#include "tst_mymodule.moc"
.pro File Template
!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
}
Test plist File Template (qtpass.plist)
<?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>
Adding a New Test Suite
- Create directory:
tests/auto/<name>/
- Add
<name>.pro file (copy pattern above)
- Add
qtpass.plist (copy from model/)
- Add
tst_<name>.cpp test file
- Add to
tests/auto/auto.pro: SUBDIRS += <name>
- Rebuild:
qmake6 -r && make -j4
- Run:
make check
Best Practices
Test Naming
- Test file:
tst_<classname>.cpp
- Test class:
tst_<classname>
- Test methods:
testMethodName() or methodDoesWhat()
Test Organization
initTestCase() - Setup (runs once before all tests)
init() - Setup before each test
- Test methods in logical order
cleanupTestCase() - Teardown (runs once after all tests)
Assertions
QVERIFY(condition);
QVERIFY2(condition, "failure message");
QCOMPARE(actual, expected);
QCOMPARE2(actual, expected, "message");
QVERIFY2(output.contains("needle"), "should contain needle");
QCOMPARE(QString("hello").toUpper(), QString("HELLO"));
Never use || in assertions
Bad (tautology or ambiguous):
QVERIFY(result == expected || result == INVALID);
QVERIFY(result != INVALID || result == INVALID);
Good - just call the method to verify it doesn't crash:
simpleTransaction trans;
trans.transactionAdd(Enums::PASS_INSERT);
trans.transactionIsOver(Enums::PASS_INSERT);
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);
QtTest Macros
QFAIL("message") - Fail with message
QSKIP("message") - Skip test
QCOMPARE(a, b) - Assert equality
QVERIFY(a) - Assert true
QVERIFY2(a, msg) - Assert with message
Windows Compatibility
void tst_executor::unixOnlyTest() {
#ifndef Q_OS_WIN
#endif
}
Path Comparison on Windows
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";
QVERIFY2(QDir::cleanPath(path) == QDir::cleanPath(expected),
qPrintable(QString("Expected %1, got %2")
.arg(QDir::cleanPath(expected), QDir::cleanPath(path))));
}
Test Settings Pollution
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
void tst_settings::initTestCase() {
QString portable_ini = QCoreApplication::applicationDirPath() +
QDir::separator() + "qtpass.ini";
bool isPortableMode = QFile::exists(portable_ini);
if (isPortableMode) {
QtPassSettings::getInstance()->sync();
QString settingsFile = QtPassSettings::getInstance()->fileName();
m_settingsBackupPath = settingsFile + ".bak";
QFile::remove(m_settingsBackupPath);
QFile::copy(settingsFile, m_settingsBackupPath);
} else {
qWarning() << "Non-portable mode detected. Tests may modify registry settings.";
}
}
void tst_settings::cleanupTestCase() {
if (isPortableMode && !m_settingsBackupPath.isEmpty()) {
QString settingsFile = QtPassSettings::getInstance()->fileName();
QFile::remove(settingsFile);
QFile::copy(m_settingsBackupPath, settingsFile);
QFile::remove(m_settingsBackupPath);
}
}
Key points:
- Only backup in portable mode (file-based settings)
- On registry mode (Windows non-portable), warn but cannot back up
- Always restore after tests to prevent pollution
Gitleaks-Safe Test Values
- DON'T: "ABC123DEF456", "sk-xxx", real API keys
- DO: "testkey123", "/usr/bin/pass", "example.com"
Qt5/Qt6 Compatibility
When checking variant types, prefer canConvert<T>() over metaType().id() for broader compatibility:
QVERIFY(displayData.metaType().id() == QMetaType::QString);
QVERIFY(displayData.canConvert<QString>());
Temporary Files/Directories
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();
}
Testing Getters with Default Parameters
When testing settings that have getters with default parameters, pass a different default value to verify persistence:
setter(testValue);
QCOMPARE(getter(testValue), testValue);
setter(testValue);
QCOMPARE(getter(!testValue), testValue);
QCOMPARE(getter(-1), testValue);
QCOMPARE(getter(QString()), testValue);
Compound Types Don't Always Fit Data-Driven
Data-driven tests work well for simple bool/int/string settings. Compound types like PasswordConfiguration often don't fit:
- Different tests test different fields (length vs selected vs characters)
- Nested data (QMap, structs) complicates the table
- Keep tests explicit and readable over forcing a pattern
Qt Test Macro Reminder
Always use the proper Qt Test macros:
QCOMPARE(actual, expected);
QVERIFY(condition);
QVERIFY2(condition, "failure message");
COMPARE(actual, expected);
QQCOMPARE(actual, expected);
Testable Source Files
src/util.h/cpp
normalizeFolderPath() - Path normalization
protocolRegex() - URL detection
endsWithGpg() - Extension matching
newLinesRegex() - Newline detection
src/filecontent.h/cpp
FileContent::parse() - Parse password file
getPassword() - Get main password
getNamedValues() - Get key:value fields
getRemainingData() - Non-template fields
getRemainingDataForDisplay() - Display-safe (hides OTP)
NamedValues::takeValue() - Extract and remove value
src/passwordconfiguration.h
PasswordConfiguration class
- Character set definitions
- Length configuration
src/executor.h/cpp
Executor::executeBlocking() - Run command synchronously
- Returns exit code, captures output/stderr
src/storemodel.h/cpp
StoreModel extends QFileSystemModel
setModelAndStore() - Initialize with model and path
data() - Returns display name (removes .gpg)
flags() - Item flags
lessThan() - Sorting
src/qtpasssettings.h
- 26+ settings: isUseGit(), setUseGit(), isUseMonospace(), etc.
CI Integration
Tests run via make check in CI. Coverage reported with make lcov.
Linting
See qtpass-linting skill for full CI workflow. Pattern:
act push -W .github/workflows/linter.yml -j build
Web/Config Files (prettier)
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"
C++ (clang-format)
clang-format --style=file --dry-run src/main.cpp
clang-format --style=file -i src/main.cpp
Project Lint
make check
Debugging Failed Tests
./tests/auto/util/tst_util testName
./tests/auto/util/tst_util -v2
./tests/auto/util/tst_util -vs