eos-unittest 是白盒测试。
eos项目每次build的时候都会自动编译链接eos/unittest下的源文件,生成出eos/build/unittests/unit_test。后者是一个二进制源程序,运行会自动运行所有测试用例。
unit_test --list_content 枚举所有测试套及测试用例。
./unit_test -t eosio_system_tests/stake_unstake 单独测试eosio_system_tests测试套的stake_unstake测试用例。
eos-unittest采用的是boost库的单元测试套件。
BOOST_AUTO_TEST_SUITE(测试套名称) 声明测试套,放在定义最前面
BOOST_AUTO_TEST_SUITE_END() 测试套结束
BOOST_FIXTURE_TEST_CASE(测试用例名,测试类名) FIXTURE测试用例
class template_test_class : public TESTER
{
// ...
};
BOOST_AUTO_TEST_SUITE(template_test_suite)
BOOST_FIXTURE_TEST_CASE(template_test_case1, template_test_class) try {
// ...
} FC_LOG_AND_RETHROW()
BOOST_AUTO_TEST_SUITE_END()
base_tester类,测试类的基类。该类提供了测试框架的主要接口。
tester类继承于base_tester类,简单地拓展了base_tester(添加了3个跟区块相关的方法)。
我们自定义的测试类需要继承tester。
获取区块时间
control->head_block_time().sec_since_epoch()
BOOST_REQUIRE_EQUAL 常用与两个简单对象(如asset)对比。
REQUIRE_MATCHING_OBJECT 常用与两个variant(尤其是mutable_variant_ojbect)对比。
void base_tester::produce_blocks( uint32_t n = 1, bool empty = false );
// exp
produce_blocks();
produce_blocks(3);
vector<transaction_trace_ptr> base_tester::create_accounts(
vector<account_name> names,
bool multisig = false,
bool include_code = true
)
// exp
create_accounts({N(hello), N(world)})
// WAVM
void set_code(
account_name name,
const char* wast,
const private_key_type* signer = nullptr
);
// WABT
void set_code(
account_name name,
const vector<uint8_t> wasm,
const private_key_type* signer = nullptr
);
void set_abi(
account_name name,
const char* abi_json,
const private_key_type* signer = nullptr
);
// exp
#include
#include
// ...
set_code( N(eosio.token), eosio_token_wast );
set_abi( N(eosio.token), eosio_token_abi );
eos项目自带的eosio_build.sh会额外编译生成两个特殊hpp文件:
在单元测试中,我们经常要对持久化数据表的数据进行反序列化,以方便我们对数据结构及相关成员的读取和校验。
// 初始化序列化器
// 注意下面的token_abi_ser.set_abi函数
const auto& accnt = control->db().get<account_object,by_name>( N(eosio.token) );
abi_def abi;
BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt.abi, abi), true);
token_abi_ser.set_abi(abi, abi_serializer_max_time);
// 初始化完了以后,就可以用来序列化持久化的数据表
// 注意下面的token_abi_ser.binary_to_variant函数
{
// ...
vector<char> data = get_row_by_account( N(eosio.token), symbol_code, N(stat), symbol_code );
return data.empty() ? fc::variant() : token_abi_ser.binary_to_variant( "currency_stats", data, abi_serializer_max_time );
}
有三种常用类型,用于弱化对象的类型,用一种通用的格式去标识所有智能合约中定义的数据结构。
variant常用于数据的序列化与反序列化,表示一种通用的数据类型。
variant_object更像是一个对象的容器。
mutable_variant_object常用于定义复杂可嵌套的数据格式,可以通过as模板方法转换类型,[]访问成员。
在eos-unittest中,有N种创建ACTION的方法:
class base_tester {
public:
/*
两个push_transaction的接口
一个接受packed_transaction,zlib压缩,已签名
另一个接受signed_transaction,无压缩
*/
transaction_trace_ptr push_transaction(
packed_transaction& trx,
fc::time_point deadline = fc::time_point::maximum(),
uint32_t billed_cpu_time_us = DEFAULT_BILLED_CPU_TIME_US
);
transaction_trace_ptr push_transaction(
signed_transaction& trx,
fc::time_point deadline = fc::time_point::maximum(),
uint32_t billed_cpu_time_us = DEFAULT_BILLED_CPU_TIME_US
);
/*
四个push_action接口,会自动打包成一个transaction:
1. 接收action对象和单一授权
2. 接收各细分action的字段、单一授权
3. 接收各细分action的字段、多active授权
4. 接收各细分action的字段、多自定义授权
*/
action_result push_action(action&& cert_act, uint64_t authorizer);
transaction_trace_ptr push_action(
const account_name& code,
const action_name& acttype,
const account_name& actor,
const variant_object& data,
uint32_t expiration = DEFAULT_EXPIRATION_DELTA,
uint32_t delay_sec = 0
);
transaction_trace_ptr push_action(
const account_name& code,
const action_name& acttype,
const vector<account_name>& actors,
const variant_object& data,
uint32_t expiration = DEFAULT_EXPIRATION_DELTA,
uint32_t delay_sec = 0
);
transaction_trace_ptr push_action(
const account_name& code,
const action_name& acttype,
const vector<permission_level>& auths,
const variant_object& data,
uint32_t expiration = DEFAULT_EXPIRATION_DELTA,
uint32_t delay_sec = 0
);
};
其中,除了一个push_action是返回action_result,其他都是返回transaction_trace_ptr,具体定义如下。是不是很眼熟?transaction_trace_ptr跟我们通过HTTP接口获得的JSON数据结果字段是一致的。action_result是截取base_action_trace::console的字符串结果。
class base_tester
{
typedef string action_result;
static action_result success() { return string(); }
static action_result error( const string& msg ) { return msg; }
static action_result wasm_assert_msg( const string& msg ) { return "assertion failure with message: " + msg; }
static action_result wasm_assert_code( uint64_t error_code ) { return "assertion failure with error code: " + std::to_string(error_code); }
};
struct base_action_trace
{
base_action_trace( const action_receipt& r ):receipt(r){}
base_action_trace(){}
action_receipt receipt;
action act;
bool context_free = false;
fc::microseconds elapsed;
uint64_t cpu_usage = 0;
string console;
uint64_t total_cpu_usage = 0; /// total of inline_traces[x].cpu_usage + cpu_usage
transaction_id_type trx_id; ///< the transaction that generated this action
uint32_t block_num = 0;
block_timestamp_type block_time;
fc::optional<block_id_type> producer_block_id;
flat_set<account_delta> account_ram_deltas;
};
struct action_trace : public base_action_trace {
using base_action_trace::base_action_trace;
vector<action_trace> inline_traces;
};
struct transaction_trace;
using transaction_trace_ptr = std::shared_ptr<transaction_trace>;
下面是几个常用的例子
action_result hello(
uint64_t param1,
uint64_t param2,
uint64_t param3
)
{
auto data = fc::variant_object(
fc::mutable_variant_object()
("param1", param1)
("param2", param2)
("param3", param3)
);
action act;
act.account = N(code);
act.name = N(hello);
act.data = abi_serializer.variant_to_binary(
abi_serializer.get_action_type(N(hello)),
data,
abi_serializer_max_time
);
return base_tester::push_action(std::move(act), uint64_t(signer));
}
BOOST_REQUIRE_EQUAL(success(), hello(param1, param2, param3));
BOOST_REQUIRE_EQUAL(error("missing authority of alice"), hello(param1, param2, param3));
transaction_trace_ptr hello(
uint64_t param1,
uint64_t param2,
uint64_t param3
)
{
// 使用第三种push_action
return base_tester::push_action(
N(code), N(hello),
vector<account_name>{ {N(actor_account1)}, {N(actor_account2)} },
fc::mutable_variant_object()
("param1", param1)
("param2", param2)
("param3", param3)
);
// PS:mutable_variant_object的语法与常规的C++语法有点不同:
// 1. 从mutable_variant_object定义开始,无须逗号“,”分隔
// 2. 每个字段以小括号定义分割,先字段键值,后字段值,类似字典
// 3. mutable_variant_object可以嵌套mutable_variant_object
}
BOOST_REQUIRE_EQUAL(success(), hello(param1, param2, param3)->action_traces[0].console);
在写测试的过程中,判定前置条件和后置条件经常需要判断持久化数据,eos-unittest框架的base_tester测试类也提供了相关的接口。
vector<char> base_tester::get_row_by_account(
uint64_t code,
uint64_t scope,
uint64_t table,
const account_name& act
) const;
下面是例子
fc::variant get_tab1(uint64_t primarykey)
{
vector<char> data = get_row_by_account(N(code), N(scope), N(tab1), primarykey);
// 通过abi_serializer反序列化元数据为结构体格式数据,可以很方便的读取表中的字段。
return data.empty() ? fc::variant() : abi_serializer::binary_to_variant("tab1_struct", data, abi_serializer_max_time);
}
在校验持久化数据的过程中,最经常用到的尤数获取账户余额,下面给出几种方法:
// 这是eosio.system_tester.cpp中的实现,实际上与get_row_by_account无太大差异
asset get_balance(const account_name& act) {
//return get_currency_balance( config::system_account_name, symbol(CORE_SYMBOL), act );
//temporary code. current get_currency_balancy uses table name N(accounts) from currency.h
//generic_currency table name is N(account).
const auto& db = control->db();
const auto* tbl = db.find<table_id_object, by_code_scope_table>(boost::make_tuple(N(eosio.token), act, N(accounts)));
share_type result = 0;
// the balance is implied to be 0 if either the table or row does not exist
if (tbl) {
const auto *obj = db.find<key_value_object, by_scope_primary>(boost::make_tuple(tbl->id, symbol(CORE_SYMBOL).to_symbol_code()));
if (obj) {
// balance is the first field in the serialization
fc::datastream<const char *> ds(obj->value.data(), obj->value.size());
fc::raw::unpack(ds, result);
}
}
return asset( result, symbol(CORE_SYMBOL) );
}
// base_tester也提供了相关的接口
asset base_tester::get_currency_balance(
const account_name& contract,
const symbol& asset_symbol,
const account_name& account
) const;