Search…
Write SCORE unit test
This document explains how to write SCORE unit-test using T-Bears unit-test framework.

Purpose

Understand how to write SCORE unit-test

Prerequisite

How to Write SCORE Unit Test Code

SCORE unittest should inherit ScoreTestCase. The SCORE unit test code works as follows
    Get SCORE instance to be tested
    Call SCORE method
    Check the result

Functions Provided by ScoreTestCase

    Instantiate SCORE
      Instantiate SCORE. So, you can access attributes and methods of SCORE like a general object.
    Set property in SCORE
      Provide the ability to set properties used inside SCORE methods.
    Mock state DB
      Store changed state caused by SCORE method invocation. The state is stored in memory, not in the file system.
    Mock event log
      It is sufficient to check that the event log has been called
    Mock internalCall(call external function in SCORE).
      The operation on the other SCORE is considered to be reliable. so, what you need is InternalCall called with specified arguments.

Methods

ScoreTestCase has 11 main methods. Inside setUp method and tearDown method, ScoreTestCase sets the environment for SCORE unit-test and clears them. So, if you want to override setUp or tearDown, you should call super() as a first statement in the overridden method.
Getting SCORE instance
    get_score_instance(score_class, owner, on_install_params)
      Get an instance of the SCORE class passed as an score_class argument
      parameters
        score_class : SCORE to instantiate
        owner : Address to set as owner of SCORE
        on_install_params : parameters of on_install_method
    update_score(prev_score_address, score_class, on_update_params)
      Update SCORE at prev_score_address with score_class instance and get updated SCORE
      parameters
        Prev_score_address : address of SCORE to update
        score_class : SCORE class to update
        on_update_params : parameters of on_update method
      Refer to test_update method in simple_score2/tests/test_unit_simple_score2.py
Setting SCORE properties
    set_msg(sender, value)
      Set msg property in SCORE
      parameters
        sender : Set sender attribute of msg to given sender argument
        value : Set value attribute of msg to given sender argument
      Refer to test_msg method in simple_score2/tests/test_unit_simple_score2.py
    set_tx(origin, timestamp, _hash, index, nonce)
      Set tx property in SCORE
      parameters
        origin : Set origin attribute of tx to given origin argument
        timestamp : Set timestamp attribute of tx to given timestamp argument
        _hash : Set hash attribute of tx to given _hash argument
        index : Set index attribute of tx to given index argument
        nonce : Set nonce attribute of tx to given nonce argument
    set_block(height, timestamp)
      Set the block property inside SCORE.
      If you pass only height, the value of block.timestamp is set to height * 2 seconds.
      When this method is called, the block_height inside the SCORE associated with the block is set to height, and the return value of the now () method is set to timestamp.
      It should be called if you use the value associated with the block information in the SCORE method you are calling.
      parameters
        height : Set height attribute of block to given height argument
        timestamp : Set timestamp attribute of block to given timestamp argument
      Refer to test_block method in simple_score2/tests/test_unit_simple_score2.py
Patching InternalCall & asserting InternalCall
    register_interface_score(internal_score_address)
      This method should be called before testing the internal_call that calls the SCORE method with an internal_score_address address.
      If you call this method, you can use the assert_internal_call method to evaluate whether internal_call is called properly with specified arguments.
      parameters
        internal_score_address : address of interface SCORE
      Refer to test_internal2 method in simple_score2/tests/test_unit_simple_score2.py
    patch_internal_method(score_address, method, new_method)
      You will use this method for patching query method to set return value.
      Since this function internally calls register_interface_score, you don't need to call register_interface_score when calling this function.
      The third argument, the new method, must be a function with the same number of arguments as the actual method.
      parameters
        internal_score_address : address of the SCORE having method to be called
        method : method to be patched
        new_method : method to patch
      Refer to test_interanl method in simple_score2/tests/test_unit_simple_score2.py
    assert_internal_call(internal_score_address, method, *params)
      assert that internal call(mock) was called with the specified arguments. Raises an AssertionError if the params passed in are different to the last call to the mock.
      parameters
        internal_score_address : address of internal call SCORE
        method : method to check
        params : params to check
      Refer to test_internal method in simple_score2/tests/test_unit_simple_score2.py
Utils
    transfer(_from, to, amount)
      Transfer icx to given 'to' address. If you pass a SCORE address to the to argument, this method calls the SCORE fallback method.
      parameters
        _from : address of sender
        to : address of receiver
        amount : amount to transfer
      Refer to test_transfer method in simple_score2/tests/test_unit_simple_score2.py
    get_balance(address)
      Query icx balance of given address.
      parameters
        address : address to query for icx balance
      Refer to test_get_balance method in simple_score2/tests/test_unit_simple_score2.py
    initialize_accounts(accounts_info)

Examples

In this example, we'll use two simple SCOREs only have getter and setter. (the first SCORE has getter and setter and another SCORE has internalCall, getter, and setter)
simple_score/simple_score.py
1
from iconservice import *
2
3
class SimpleScore(IconScoreBase):
4
5
def __init__(self, db: IconScoreDatabase) -> None:
6
super().__init__(db)
7
self.value = VarDB("value", db, value_type=str)
8
9
@eventlog()
10
def SetValue(self, value: str): pass
11
12
def on_install(self) -> None:
13
super().on_install()
14
15
def on_update(self) -> None:
16
super().on_update()
17
self.value.set("updated value")
18
19
@external(readonly=True)
20
def hello(self) -> str:
21
return "Hello"
22
23
@external
24
def setValue(self, value: str):
25
self.value.set(value)
26
27
self.SetValue(value)
28
29
@external(readonly=True)
30
def getValue(self) -> str:
31
return self.value.get()
Copied!
simple_score2/simple_score2.py
1
from iconservice import *
2
3
4
class SimpleScoreInterface(InterfaceScore):
5
@interface
6
def setValue(self, value): pass
7
8
@interface
9
def getValue(self)->str: pass
10
11
12
class SimpleScore2(IconScoreBase):
13
14
def __init__(self, db: IconScoreDatabase) -> None:
15
super().__init__(db)
16
self.value = VarDB("value1", db, value_type=str)
17
self.score_address = VarDB("score_address", db, value_type=Address)
18
19
@eventlog(indexed=0)
20
def SetValue(self, value: str): pass
21
22
@eventlog(indexed=1)
23
def SetSCOREValue(self, value: str): pass
24
25
def on_install(self, score_address: 'Address') -> None:
26
super().on_install()
27
self.score_address.set(score_address)
28
29
def on_update(self, value: str) -> None:
30
super().on_update()
31
self.value.set(value)
32
33
@external(readonly=True)
34
def getValue(self) -> str:
35
return self.value.get()
36
37
@external
38
def setValue(self, value: str):
39
self.value.set(value)
40
41
self.SetValue(value)
42
43
@external
44
def setSCOREValue(self, value: str):
45
score = self.create_interface_score(self.score_address.get(), SimpleScoreInterface)
46
score.setValue(value)
47
48
self.SetSCOREValue(value)
49
50
@external(readonly=True)
51
def getSCOREValue(self) ->str:
52
score = self.create_interface_score(self.score_address.get(), SimpleScoreInterface)
53
54
return score.getValue()
55
56
@external(readonly=True)
57
def write_on_readonly(self) ->str:
58
self.value.set('3')
59
return 'd'
60
61
# This method is for understanding the ScoreTestCase.set_msg method.
62
def t_msg(self):
63
assert self.msg.sender == Address.from_string(f"hx{'1234'*10}")
64
assert self.msg.value == 3
65
66
# This method is for understanding the ScoreTestCase.set_tx method.
67
def t_tx(self):
68
assert self.tx.origin == Address.from_string(f"hx{'1234'*10}")
69
70
# This method is for understanding the ScoreTestCase.set_block method.
71
def t_block(self):
72
assert self.block.height == 3
73
assert self.block.timestamp == 30
74
assert self.block_height ==3
75
assert self.now() == 30
Copied!
simple_score2/tests/test_unit_simple_score2.py
1
from iconservice import Address
2
from iconservice.base.exception import DatabaseException
3
from tbears.libs.scoretest.score_test_case import ScoreTestCase
4
from ..simple_score2 import SimpleScore2
5
6
class TestSimple(ScoreTestCase):
7
def setUp(self):
8
super().setUp()
9
self.mock_score_address = Address.from_string(f"cx{'1234'*10}")
10
self.score2 = self.get_score_instance(SimpleScore2, self.test_account1,
11
on_install_params={'score_address': self.mock_score_address})
12
13
self.test_account3 = Address.from_string(f"hx{'12345'*8}")
14
self.test_account4 = Address.from_string(f"hx{'1234'*10}")
15
account_info = {
16
self.test_account3: 10 ** 21,
17
self.test_account4: 10 ** 21}
18
self.initialize_accounts(account_info)
19
20
def test_set_value(self):
21
str_value = 'string_value'
22
self.score2.setValue(str_value)
23
# assert event log called with specified arguments
24
self.score2.SetValue.assert_called_with(str_value)
25
26
self.assertEqual(self.score2.getValue(), str_value)
27
28
def test_get_value_and_set_value(self):
29
# at first, value is empty string
30
self.assertEqual(self.score2.getValue(), '')
31
32
str_value = 'strValue'
33
self.score2.setValue(str_value)
34
35
self.assertEqual(self.score2.getValue(), str_value)
36
37
# try writing value inside readonly method
38
def test_write_on_readonly(self):
39
self.assertRaises(DatabaseException, self.score2.write_on_readonly)
40
41
# internal call
42
def test_internal_call(self):
43
self.patch_internal_method(self.mock_score_address, 'getValue', lambda: 150) # Patch the getValue function of SCORE at self.mock_score_address address with a function that takes no argument and returns 150
44
value = self.score2.getSCOREValue()
45
self.assertEqual(value, 150)
46
self.assert_internal_call(self.mock_score_address, 'getValue') # assert getValue in self.mock_score_address is called.
47
48
self.score2.setSCOREValue('asdf')
49
self.assert_internal_call(self.mock_score_address, 'setValue', 'asdf') # assert setValue in self.mock_score_address is called with 'asdf'
50
51
# internal call
52
def test_internal_call2(self):
53
# To determine whether a method is called properly with specified arguments, calling register_interface_score method is enough
54
self.register_interface_score(self.mock_score_address)
55
self.score2.setSCOREValue('asdf')
56
self.assert_internal_call(self.mock_score_address, 'setValue', 'asdf')
57
58
def test_msg(self):
59
self.set_msg(Address.from_string(f"hx{'1234'*10}"), 3)
60
self.score2.t_msg() # On the upper line, set the msg property to pass the assert statement so that no exception is raised.
61
62
self.set_msg(Address.from_string(f"hx{'12'*20}"), 3)
63
self.assertRaises(AssertionError, self.score2.t_msg) # On the upper line, set the msg property not to pass the assert statement, and raise an exception.
64
65
def test_tx(self):
66
self.set_tx(Address.from_string(f"hx{'1234'*10}"))
67
self.score2.t_tx() # On the upper line, set the tx property to pass the assert statement so that no exception is raised.
68
69
self.set_tx(Address.from_string(f"hx{'12'*20}"))
70
self.assertRaises(AssertionError, self.score2.t_tx) # On the upper line, set the tx property not to pass the assert statement, and raise an exception.
71
72
def test_block(self):
73
self.set_block(3, 30)
74
self.score2.t_block() # On the upper line, set the block property to pass the assert statement so that no exception is raised.
75
76
self.set_block(3)
77
self.assertRaises(AssertionError, self.score2.t_block) # On the upper line, set the block property not to pass the assert statement, and raise an exception.
78
79
def test_update(self):
80
self.score2 = self.update_score(self.score2.address, SimpleScore2, on_update_params={"value": "updated_value"})
81
self.assertEqual(self.score2.value.get(), "updated_value") # In the on_update method of SimpleScore2, set the value of the value to "updated_value".
82
83
def test_get_balance(self):
84
balance = self.get_balance(self.test_account3)
85
self.assertEqual(balance, 10**21)
86
87
def test_transfer(self):
88
# before calling transfer method, check balance of test_account3 and test_account4
89
amount = 10**21
90
balance_3 = self.get_balance(self.test_account3)
91
self.assertEqual(balance_3, amount)
92
balance_4 = self.get_balance(self.test_account4)
93
self.assertEqual(balance_4, amount)
94
95
self.transfer(self.test_account3, self.test_account4, amount)
96
# after calling transfer method, check balance of test_account3 and test_account4
97
balance_3 = self.get_balance(self.test_account3)
98
self.assertEqual(balance_3, 0)
99
balance_4 = self.get_balance(self.test_account4)
100
self.assertEqual(balance_4, amount*2)
Copied!
Run test code
1
$ tbears test simple_score2
2
........
3
----------------------------------------------------------------------
4
Ran 11 tests in 0.027s
5
6
OK
Copied!
Last modified 4mo ago