eos-unittest

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

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文件:

  • contract_name.abi.hpp
  • contract_name.wast.hpp
    里面是把abi和wast的数据转换成头文件,方便测试时应用。

设置序列化数据

在单元测试中,我们经常要对持久化数据表的数据进行反序列化,以方便我们对数据结构及相关成员的读取和校验。

// 初始化序列化器
// 注意下面的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
  • variant_object
  • mutable_variant_object

variant常用于数据的序列化与反序列化,表示一种通用的数据类型。
variant_object更像是一个对象的容器。
mutable_variant_object常用于定义复杂可嵌套的数据格式,可以通过as模板方法转换类型,[]访问成员。

提交TRX&ACTION

在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;

你可能感兴趣的:(blockchain)