Skip to content

Commit 3a75d9b

Browse files
committed
test: improve handwritten test doubles
1 parent 55fe4e5 commit 3a75d9b

13 files changed

Lines changed: 723 additions & 645 deletions

test/mocks/callcounter.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2026 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_QML_TEST_MOCKS_CALLCOUNTER_H
6+
#define BITCOIN_QML_TEST_MOCKS_CALLCOUNTER_H
7+
8+
#include <atomic>
9+
#include <string_view>
10+
11+
class CallCounter
12+
{
13+
public:
14+
explicit CallCounter(std::string_view name) : m_name{name} {}
15+
16+
CallCounter(const CallCounter&) = delete;
17+
CallCounter& operator=(const CallCounter&) = delete;
18+
19+
int operator++() { return m_calls.fetch_add(1) + 1; }
20+
int load() const { return m_calls.load(); }
21+
std::string_view Name() const { return m_name; }
22+
23+
private:
24+
std::atomic<int> m_calls{0};
25+
const std::string_view m_name;
26+
};
27+
28+
#endif // BITCOIN_QML_TEST_MOCKS_CALLCOUNTER_H

test/mocks/mocknode.h

Lines changed: 140 additions & 121 deletions
Large diffs are not rendered by default.

test/mocks/mockwallet.h

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
#include <interfaces/wallet.h>
99
#include <outputtype.h>
10+
#include <test/mocks/callcounter.h>
1011
#include <wallet/coincontrol.h>
1112
#include <wallet/types.h>
1213
#include <wallet/wallet.h>
1314

1415
#include <QtTest/qtestcase.h>
1516

16-
#include <atomic>
1717
#include <functional>
1818
#include <source_location>
1919
#include <sstream>
@@ -88,54 +88,63 @@ class StubWallet : public interfaces::Wallet
8888
std::unique_ptr<interfaces::Handler> handleCanGetAddressesChanged(CanGetAddressesChangedFn) override { return {}; }
8989
};
9090

91+
/**
92+
* Configurable wallet test double with permissive defaults.
93+
*
94+
* Unconfigured methods return their StubWallet values. Tests register explicit
95+
* expectations for calls that are part of the behavior under test.
96+
*/
9197
class MockWallet : public StubWallet
9298
{
9399
public:
94100
class Verification
95101
{
96102
public:
97-
Verification(MockWallet& wallet, std::source_location where)
98-
: m_wallet{&wallet}, m_where{where}
103+
explicit Verification(MockWallet& wallet)
104+
: m_wallet{&wallet}
99105
{
100106
}
101107

102108
Verification(const Verification&) = delete;
103109
Verification& operator=(const Verification&) = delete;
104110
Verification(Verification&& other) noexcept
105-
: m_wallet{std::exchange(other.m_wallet, nullptr)}, m_where{other.m_where}
111+
: m_wallet{std::exchange(other.m_wallet, nullptr)}
106112
{
107113
}
108114

109115
~Verification()
110116
{
111117
if (!m_wallet) return;
112-
const std::string failure{m_wallet->CallExpectationFailure()};
113-
if (!failure.empty()) {
114-
QTest::qFail(failure.c_str(), m_where.file_name(), static_cast<int>(m_where.line()));
118+
for (const auto& failure : m_wallet->VerificationFailures()) {
119+
QTest::qFail(failure.message.c_str(), failure.where.file_name(), static_cast<int>(failure.where.line()));
115120
}
116121
}
117122

118123
private:
119124
MockWallet* m_wallet;
120-
std::source_location m_where;
121125
};
122126

123-
Verification VerifyOnExit(std::source_location where = std::source_location::current())
127+
[[nodiscard]] Verification VerifyOnExit()
124128
{
125-
return Verification{*this, where};
129+
return Verification{*this};
126130
}
127131

128-
void ExpectCallCount(const std::atomic<int>& calls, int expected, const char* name)
132+
void ExpectExactly(const CallCounter& counter, int expected, std::source_location where = std::source_location::current())
129133
{
130-
m_call_expectations.push_back(CallExpectation{&calls, calls.load(), expected, expected, name});
134+
m_call_expectations.push_back(CallExpectation{&counter, counter.load(), expected, expected, where});
131135
}
132136

133-
void ExpectAtLeast(const std::atomic<int>& calls, int minimum, const char* name)
137+
void ExpectNoCalls(const CallCounter& counter, std::source_location where = std::source_location::current())
134138
{
135-
m_call_expectations.push_back(CallExpectation{&calls, calls.load(), minimum, std::nullopt, name});
139+
ExpectExactly(counter, 0, where);
136140
}
137141

138-
std::function<util::Result<CTransactionRef>(const std::vector<wallet::CRecipient>&, const wallet::CCoinControl&, bool, int&, CAmount&)> createTransactionHandler;
142+
void ExpectAtLeast(const CallCounter& counter, int minimum, std::source_location where = std::source_location::current())
143+
{
144+
m_call_expectations.push_back(CallExpectation{&counter, counter.load(), minimum, std::nullopt, where});
145+
}
146+
147+
std::function<util::Result<CTransactionRef>(const std::vector<wallet::CRecipient>&, const wallet::CCoinControl&, bool, int&, CAmount&)> create_transaction_fn;
139148
std::function<CTxDestination(OutputType, const std::string&)> get_new_destination_fn;
140149
std::function<std::set<interfaces::WalletTx>()> get_wallet_txs_fn;
141150
std::function<CAmount()> get_balance_fn;
@@ -149,22 +158,26 @@ class MockWallet : public StubWallet
149158
std::function<bool(CMutableTransaction&)> sign_bump_transaction_fn;
150159
std::function<bool(const Txid&, CMutableTransaction&&, std::vector<bilingual_str>&, Txid&)> commit_bump_transaction_fn;
151160

152-
std::atomic<int> get_new_destination_calls{0};
153-
std::atomic<int> get_wallet_txs_calls{0};
154-
std::atomic<int> get_balance_calls{0};
155-
std::atomic<int> get_available_balance_calls{0};
156-
std::atomic<int> get_required_fee_calls{0};
157-
std::atomic<int> list_coins_calls{0};
158-
std::atomic<int> get_default_address_type_calls{0};
159-
std::atomic<int> handle_transaction_changed_calls{0};
160-
std::atomic<int> transaction_can_be_bumped_calls{0};
161-
std::atomic<int> create_bump_transaction_calls{0};
162-
std::atomic<int> sign_bump_transaction_calls{0};
163-
std::atomic<int> commit_bump_transaction_calls{0};
161+
struct Calls
162+
{
163+
CallCounter getNewDestination{"getNewDestination"};
164+
CallCounter createTransaction{"createTransaction"};
165+
CallCounter getWalletTxs{"getWalletTxs"};
166+
CallCounter getBalance{"getBalance"};
167+
CallCounter getAvailableBalance{"getAvailableBalance"};
168+
CallCounter getRequiredFee{"getRequiredFee"};
169+
CallCounter listCoins{"listCoins"};
170+
CallCounter getDefaultAddressType{"getDefaultAddressType"};
171+
CallCounter handleTransactionChanged{"handleTransactionChanged"};
172+
CallCounter transactionCanBeBumped{"transactionCanBeBumped"};
173+
CallCounter createBumpTransaction{"createBumpTransaction"};
174+
CallCounter signBumpTransaction{"signBumpTransaction"};
175+
CallCounter commitBumpTransaction{"commitBumpTransaction"};
176+
} calls;
164177

165178
util::Result<CTxDestination> getNewDestination(const OutputType type, const std::string& label) override
166179
{
167-
++get_new_destination_calls;
180+
++calls.getNewDestination;
168181
if (get_new_destination_fn) return get_new_destination_fn(type, label);
169182
return util::Error{Untranslated("no get_new_destination_fn installed")};
170183
}
@@ -174,10 +187,11 @@ class MockWallet : public StubWallet
174187
bool sign,
175188
std::optional<unsigned int>) override
176189
{
177-
if (createTransactionHandler) {
190+
++calls.createTransaction;
191+
if (create_transaction_fn) {
178192
int change_pos{-1};
179193
CAmount fee{0};
180-
auto result = createTransactionHandler(recipients, coin_control, sign, change_pos, fee);
194+
auto result = create_transaction_fn(recipients, coin_control, sign, change_pos, fee);
181195
if (!result) {
182196
return util::Error{util::ErrorString(result)};
183197
}
@@ -187,94 +201,100 @@ class MockWallet : public StubWallet
187201
change_pos >= 0 ? std::optional<unsigned int>{static_cast<unsigned int>(change_pos)} : std::nullopt,
188202
FeeCalculation{}};
189203
}
190-
return util::Error{Untranslated("no createTransactionHandler installed")};
204+
return util::Error{Untranslated("no create_transaction_fn installed")};
191205
}
192206

193207
std::set<interfaces::WalletTx> getWalletTxs() override
194208
{
195-
++get_wallet_txs_calls;
209+
++calls.getWalletTxs;
196210
return get_wallet_txs_fn ? get_wallet_txs_fn() : std::set<interfaces::WalletTx>{};
197211
}
198212

199213
CAmount getBalance() override
200214
{
201-
++get_balance_calls;
215+
++calls.getBalance;
202216
return get_balance_fn ? get_balance_fn() : 0;
203217
}
204218

205219
CAmount getAvailableBalance(const wallet::CCoinControl& coin_control) override
206220
{
207-
++get_available_balance_calls;
221+
++calls.getAvailableBalance;
208222
return get_available_balance_fn ? get_available_balance_fn(coin_control) : 0;
209223
}
210224

211225
CAmount getRequiredFee(unsigned int tx_bytes) override
212226
{
213-
++get_required_fee_calls;
227+
++calls.getRequiredFee;
214228
return get_required_fee_fn ? get_required_fee_fn(tx_bytes) : 0;
215229
}
216230

217231
CoinsList listCoins() override
218232
{
219-
++list_coins_calls;
233+
++calls.listCoins;
220234
return list_coins_fn ? list_coins_fn() : CoinsList{};
221235
}
222236

223237
OutputType getDefaultAddressType() override
224238
{
225-
++get_default_address_type_calls;
239+
++calls.getDefaultAddressType;
226240
return get_default_address_type_fn ? get_default_address_type_fn() : OutputType::BECH32;
227241
}
228242

229243
std::unique_ptr<interfaces::Handler> handleTransactionChanged(TransactionChangedFn fn) override
230244
{
231-
++handle_transaction_changed_calls;
245+
++calls.handleTransactionChanged;
232246
return handle_transaction_changed_fn ? handle_transaction_changed_fn(std::move(fn)) : nullptr;
233247
}
234248

235249
bool transactionCanBeBumped(const Txid& txid) override
236250
{
237-
++transaction_can_be_bumped_calls;
251+
++calls.transactionCanBeBumped;
238252
return transaction_can_be_bumped_fn ? transaction_can_be_bumped_fn(txid) : false;
239253
}
240254

241255
bool createBumpTransaction(const Txid& txid, const wallet::CCoinControl& coin_control, std::vector<bilingual_str>& errors,
242256
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx) override
243257
{
244-
++create_bump_transaction_calls;
258+
++calls.createBumpTransaction;
245259
return create_bump_transaction_fn ? create_bump_transaction_fn(txid, coin_control, errors, old_fee, new_fee, mtx) : false;
246260
}
247261

248262
bool signBumpTransaction(CMutableTransaction& mtx) override
249263
{
250-
++sign_bump_transaction_calls;
264+
++calls.signBumpTransaction;
251265
return sign_bump_transaction_fn ? sign_bump_transaction_fn(mtx) : false;
252266
}
253267

254268
bool commitBumpTransaction(const Txid& txid, CMutableTransaction&& mtx, std::vector<bilingual_str>& errors, Txid& bumped_txid) override
255269
{
256-
++commit_bump_transaction_calls;
270+
++calls.commitBumpTransaction;
257271
return commit_bump_transaction_fn ? commit_bump_transaction_fn(txid, std::move(mtx), errors, bumped_txid) : false;
258272
}
259273

260274
private:
261275
struct CallExpectation
262276
{
263-
const std::atomic<int>* calls;
277+
const CallCounter* counter;
264278
int baseline;
265279
int minimum;
266280
std::optional<int> maximum;
267-
std::string name;
281+
std::source_location where;
282+
};
283+
284+
struct VerificationFailure
285+
{
286+
std::string message;
287+
std::source_location where;
268288
};
269289

270-
std::string CallExpectationFailure() const
290+
std::vector<VerificationFailure> VerificationFailures() const
271291
{
272-
std::ostringstream message;
292+
std::vector<VerificationFailure> failures;
273293
for (const CallExpectation& expectation : m_call_expectations) {
274-
const int actual{expectation.calls->load() - expectation.baseline};
294+
const int actual{expectation.counter->load() - expectation.baseline};
275295
if (actual < expectation.minimum || (expectation.maximum && actual > *expectation.maximum)) {
276-
if (message.tellp() > 0) message << '\n';
277-
message << "Expected " << expectation.name << " calls to be ";
296+
std::ostringstream message;
297+
message << "Expected " << expectation.counter->Name() << " calls to be ";
278298
if (expectation.maximum && expectation.minimum == *expectation.maximum) {
279299
message << expectation.minimum;
280300
} else if (expectation.maximum) {
@@ -283,9 +303,10 @@ class MockWallet : public StubWallet
283303
message << "at least " << expectation.minimum;
284304
}
285305
message << ", actual " << actual;
306+
failures.push_back({message.str(), expectation.where});
286307
}
287308
}
288-
return message.str();
309+
return failures;
289310
}
290311

291312
std::vector<CallExpectation> m_call_expectations;

test/mocks/stubnode.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class StubNode : public interfaces::Node
130130

131131
protected:
132132
virtual void UnhandledCall(const char*) {}
133+
interfaces::WalletLoader& FallbackWalletLoader() { return m_wallet_loader; }
133134

134135
private:
135136
StubWalletLoader m_wallet_loader;

test/test_banlistmodel.cpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ void BanListModelTests::refreshPopulatesRolesAndRows()
6464

6565
model.refresh();
6666

67-
QCOMPARE(node.get_banned_calls.load(), 1);
67+
QCOMPARE(node.calls.getBanned.load(), 1);
6868
QCOMPARE(model.count(), 2);
6969
QCOMPARE(model.rowCount(), 2);
7070
QCOMPARE(model.rowCount(model.index(0, 0)), 0);
@@ -112,8 +112,8 @@ void BanListModelTests::unbanAtTargetsSelectedSubnet()
112112
return true;
113113
};
114114
QVERIFY(model.unbanAt(0));
115-
QCOMPARE(node.get_banned_calls.load(), 1);
116-
QCOMPARE(node.unban_calls.load(), 1);
115+
QCOMPARE(node.calls.getBanned.load(), 1);
116+
QCOMPARE(node.calls.unban.load(), 1);
117117
QVERIFY(received_expected_subnet);
118118
}
119119

@@ -133,8 +133,8 @@ void BanListModelTests::unbanAtReturnsFalseWhenNodeRejectsSubnet()
133133

134134
node.unban_fn = [](const CSubNet&) { return false; };
135135
QVERIFY(!model.unbanAt(0));
136-
QCOMPARE(node.get_banned_calls.load(), 1);
137-
QCOMPARE(node.unban_calls.load(), 1);
136+
QCOMPARE(node.calls.getBanned.load(), 1);
137+
QCOMPARE(node.calls.unban.load(), 1);
138138
}
139139

140140
void BanListModelTests::unbanAtIgnoresInvalidRows()
@@ -153,8 +153,8 @@ void BanListModelTests::unbanAtIgnoresInvalidRows()
153153

154154
QVERIFY(!model.unbanAt(-1));
155155
QVERIFY(!model.unbanAt(42));
156-
QCOMPARE(node.get_banned_calls.load(), 1);
157-
QCOMPARE(node.unban_calls.load(), 0);
156+
QCOMPARE(node.calls.getBanned.load(), 1);
157+
QCOMPARE(node.calls.unban.load(), 0);
158158
}
159159

160160
#ifdef BITCOINQML_NO_TEST_MAIN

0 commit comments

Comments
 (0)