Brownie는 hypothesis
프레임워크를 활용하여 상태 저장 테스트를 용합니다.
이 섹션의 많은 내용은 공식 hypothesis.works 웹사이트를 기반으로 합니다. 상태 기반 테스팅에 대해 더 자세히 알아보려면 다음 글을 읽어보시기를 추천합니다:
- David R. MacIver의 Rule Based Stateful Testing
- Nicholas Chammas의 Solving the Water Jug Problem from Die Hard 3 with TLA+ and Hypothesis
- Hypothesis 문서의 상태 기반 테스팅
규칙 기반 상태 기계
상태 기계는 상태 기반 테스트 내에서 사용되는 클래스입니다. 초기 테스트 상태, 테스트가 실행될 구조를 간략하게 설명하는 여러 작업, 실행 중에 위반해서는 안 되는 불변성을 정의합니다.
RuleBasedStateMachine
을 서브클래스화해서는 안 됩니다.규칙
모든 상태 기계의 핵심은 하나 이상의 규칙입니다. 규칙은 @given
기반 테스트와 매우 유사한 클래스 메서드입니다. 이들은 전략에서 추출한 값을 받아 사용자 정의 테스트 함수에 전달합니다. 핵심적인 차이점은 @given
기반 테스트가 독립적으로 실행되는 반면 규칙은 연결되어 실행될 수 있다는 것입니다. 단일 상태 테스트 실행은 여러 규칙 호출을 필요로 하며, 이들은 다양한 방식으로 상호작용할 수 있습니다.
rule
로 이름 지어진 또는 rule_
로 시작하는 모든 상태 머신 메서드는 규칙으로 처리됩니다.
class StateMachine:
def rule_one(self):
# performs a test action
def rule_two(self):
# performs another, different test action
Initializers
initializer는 특별한 종류의 규칙입니다. 이들 규칙은 실행이 시작되기 전에 최대 한 번 실행될 것이 보장됩니다. 다른 일반적인 규칙이 호출되기 전에 실행됩니다. 이들 규칙은 어떤 순서로든 호출될 수 있으며, 호출되지 않을 수도 있으며, 실행이 될 때마다 순서는 달라질 수 있습니다.
initialize
로 시작되거나 initialize_
로 시작하는 상태 기계 메서드는 모두 initializer로 처리됩니다.
class StateMachine:
def initialize(self):
# this method may or may not be called prior to rule_two
def rule(self):
# once this method is called, initialize will not be called during the test run
전략
상태 머신은 규칙에 데이터를 제공하기 위해 하나 이상의 전략을 포함해야 합니다.
전략은 일반적으로 첫 번째 함수 이전에 클래스 수준에서 정의되어야 합니다. 전략에는 임의의 이름을 지정할 수 있습니다.
pytest 테스트 내에서 fixture가 작동하는 방식과 유사하게, 상태 머신 규칙은 인수 내에서 전략을 참조하여 전달받습니다. 다음 예제에서 이를 보여줍니다:
class StateMachine:
st_uint = strategy('uint256')
st_bytes32 = strategy('bytes32')
def initialize(self, st_uint):
# this method draws from the uint256 strategy
def rule(self, st_uint, st_bytes32):
# this method draws from both strategies
def rule_two(self, value="st_uint", othervalue="st_uint"):
# this method draws from the same strategy twice
불변식
규칙과 함께, 상태 기계 종종 불변식을 정의합니다. 이는 규칙에 의해 수행되는 어떤 작업이 있더라도 변경되지 않아야 하는 속성입니다. 각 규칙이 실행된 후, 모든 불변식 메서드가 항상 호출되어 테스트가 실패하지 않았는지 확인합니다.
invariant
또는 invariant_
로 시작하는 상태 머신 메서드는 불변식으로 처리됩니다. 불변식은 상태의 올바름을 검증하기 위해 사용됩니다. 불변식은 전략을 수신할 수 없습니다.
class StateMachine:
def rule_one(self):
pass
def rule_two(self):
pass
def invariant(self):
# assertions in this method should always pass regardless# of actions in both rule_one and rule_two
설정 및 해제
상태 기계는 선택적으로 설정 및 해제 절차를 포함할 수 있습니다. pytest fixtures와 유사하게, 설정 및 해제 방법을 사용하여 테스트 및 실행 단위로 로직을 실행할 수 있습니다.
classmethodStateMachine.__init__
(cls, *args)
이 메소드는 첫 번째 테스트 실행 전에 취한 체인 스냅샷 이전에 한 번 호출됩니다. 이는 클래스 방식으로 실행됩니다 - 상태 기계에 대한 변경 사항은 테스트의 모든 실행에서 지속됩니다. __init__
메소드만이 외부 데이터를 상태 기계로 전달하는 데 사용될 수 있습니다. 다음 예제에서는 accounts fixture와 토큰 계약의 배포된 인스턴스를 전달하는 데 사용합니다:
class StateMachine:
def __init__(cls, accounts, token):
cls.accounts = accounts
cls.token = token
def test_stateful(Token, accounts, state_machine):
token = Token.deploy("Test Token", "TST", 18, 1e23, {'from': accounts[0]})
# state_machine forwards all the arguments to StateMachine.__init__state_machine(StateMachine, accounts, token)
classmethodStateMachine.setup
(self)
이 메서드는 각 테스트 실행의 시작에서 호출되며, 체인이 스냅샷으로 복원된 직후 즉시 호출됩니다. setup
중에 적용된 변경 사항은 다음 실행에만 영향을 미칩니다.
classmethodStateMachine.teardown
(self)
이 방법은 각 성공적인 테스트 실행 후 체인 되돌리기 이전에 호출됩니다. 실패한 경우 teardown
은 호출되지 않습니다.
classmethodStateMachine.teardown_final
(cls)
이 방법은 최종 테스트 실행이 완료되고 체인이 되돌려진 후에 호출됩니다. teardown_final
은 테스트가 성공했는지 실패했는지에 관계없이 호출됩니다.
테스트 실행 순서
Brownie 상태 기반 테스트는 다음 순서로 실행됩니다.
- 모든 pytest fixtures의 설정 단계는 일반적인 순서대로 실행됩니다.
- StateMachine.__init__ 메서드가 존재하는 경우 호출됩니다.
- 현재 체인 상태의 스냅샷이 생성됩니다.
- StateMachine.setup 메소드가 존재하는 경우 호출됩니다.
- 어떠한 특정한 순서 없이, 하나 이상의 StateMachine initialize 메소드가 호출됩니다.
- 어떠한 특정한 순서 없이, 하나 이상의 StateMachine rule 메소드가 호출됩니다.
- 각각의 initialize 및 rule 이후에, 모든 StateMachine invariant 메소드가 호출됩니다.
- StateMachine.teardown 메소드가 존재하는 경우 호출됩니다.
- 체인은 3단계에서 생성한 스냅샷으로 되돌아갑니다.
- 단계 4-9는 50회 또는 테스트가 실패할 때까지 반복됩니다.
- StateMachine.teardown_final 메소드가 존재하는 경우 호출됩니다.
- 모든 pytest fixtures의 해제 단계는 일반적인 순서대로 실행됩니다.
상태 기반 테스트 작성하기
상태 기반 테스트를 작성하려면:
- 상태 기계 클래스를 생성합니다.
state_machine
픽스처를 포함한 일반 pytest 스타일의 테스트를 만듭니다.
- 테스트 내에서 첫 번째 인자로 상태 기계를 사용하여
state_machine
을 호출합니다.
brownie.test.stateful.state_machine
(state_machine_class, *args, settings=None)
상태 기반 테스트를 실행합니다. • state_machine_class
: 테스트에서 사용할 상태 머신 클래스입니다. 클래스 자체를 전달하고 인스턴스를 전달하지 않도록 주의하세요. • *args
: 여기에서 제공한 모든 인수는 상태 머신의 __init__
메소드로 전달됩니다. • settings
: 이 테스트에서만 기본값을 대체할 Hypothesis settings의 선택적인 dict입니다. 이 메소드는 pytest fixture로 사용 가능한 state_machine
입니다.
기본 예제
기본적인 예제로, 다음 Vyper Depositer
컨트랙트를 테스트하기 위해 상태 기계 생성합니다. 이 컨트랙트는 두 개의 함수와 public 매핑을 가진 매우 간단한 컨트랙트입니다. 누구나 deposit_for
메서드를 사용하여 다른 계정을 위해 이더를 예치할 수 있으며, withdraw_from
을 사용하여 예치된 이더를 출금할 수 있습니다.
deposited: public(HashMap[address, uint256])
@external
@payable
def deposit_for(_receiver: address) -> bool:
self.deposited[_receiver] += msg.value
return True
@external
def withdraw_from(_value: uint256) -> bool:
assert self.deposited[msg.sender] >= _value, "Insufficient balance"
self.deposited[msg.sender] = _value
send(msg.sender, _value)
return True
만약 자세히 살펴보면 계약 코드에서 중요한 문제점을 발견할 수 있습니다. 그렇지 않으면 걱정하지 마세요! 우리는 테스트를 사용하여 찾을 것입니다.
여기에는 계약을 테스트하는 데 사용할 수 있는 상태 기계 및 테스트 함수가 있습니다.
import brownie
from brownie.test import strategy
class StateMachine:
value = strategy('uint256', max_value="1 ether")
address = strategy('address')
def __init__(cls, accounts, Depositer):
# deploy the contract at the start of the testcls.accounts = accounts
cls.contract = Depositer.deploy({'from': accounts[0]})
def setup(self):
# zero the deposit amounts at the start of each test runself.deposits = {i: 0 for i in self.accounts}
def rule_deposit(self, address, value):
# make a deposit and adjust the local recordself.contract.deposit_for(address, {'from': self.accounts[0], 'value': value})
self.deposits[address] += value
def rule_withdraw(self, address, value):
if self.deposits[address] >= value:
# make a withdrawal and adjust the local recordself.contract.withdraw_from(value, {'from': address})
self.deposits[address] -= value
else:
# attempting to withdraw beyond your balance should revertwith brownie.reverts("Insufficient balance"):
self.contract.withdraw_from(value, {'from': address})
def invariant(self):
# compare the contract deposit amounts with the local recordfor address, amount in self.deposits.items():
assert self.contract.deposited(address) == amount
def test_stateful(Depositer, accounts, state_machine):
state_machine(StateMachine, accounts, Depositer)
이 테스트가 실행되면 어설션 중 하나를 위반하는 상태가 발생할 때까지 주어진 전략에서 임의의 데이터를 사용하여 rule_deposit
및 rule_withdraw
를 호출합니다. 이 경우 오류를 재현하는 최단 경로와 가능한 가장 작은 데이터 세트를 찾기 위해 테스트를 반복합니다. 마지막으로 향후 테스트에서 사용할 실패 조건을 저장한 다음 다음과 같은 출력을 제공합니다:
def invariant(self):
for address, amount in self.deposits.items():
> assert self.contract.deposited(address) == amount
E AssertionError: assert 0 == 1
Falsifying example:
state = BrownieStateMachine()
state.rule_deposit(address=<Account '0x33A4622B82D4c04a53e170c638B944ce27cffce3'>, value=1)
state.rule_withdraw(address=<Account '0x33A4622B82D4c04a53e170c638B944ce27cffce3'>, value=0)
state.teardown()
이를 통해 오류로 이어지는 호출 시퀀스를 확인할 수 있으며, 실패한 어설션은 self.contract.deposited(address)
가 1이 될 것으로 예상했는데 0이라는 것입니다. 컨트랙트가 인출 함수 내에서 잔액을 잘못 조정하고 있다는 것을 유추할 수 있습니다. 해당 함수를 살펴봅시다:
@external
def withdraw_from(_value: uint256) -> bool:
assert self.deposited[msg.sender] >= _value, "Insufficient balance"
self.deposited[msg.sender] = _value
send(msg.sender, _value)
return True
12번 라인에서는 _value
를 빼는 대신 잔액이 _value
로 설정됩니다. 버그를 찾았습니다!
추가 예시
상태 기반 테스트를 사용하는 저장소에 대한 링크 몇 가지를 여기에 제공합니다. 여기에 포함되고 싶은 프로젝트가 있다면, 자유롭게 이 문서를 편집하고 풀 리퀘스트를 열거나 Gitter에서 알려주세요.
- celioggr/erc20-pbt : ERC-20 계약의 정확성과 규정 준수를 평가하기 위한 속성 기반 테스트 프레임워크입니다.
- iamdefinitelyahuman/NFToken: ERC20 표준의 비교 가능하지 않은 구현입니다.
- apguerrera/DreamFrames: 영화 안에서 프레임을 구매 및 판매합니다.
- curvefi/curve-dao-contracts: Curve DAO에서 사용되는 Vyper 계약
상태 기반 테스트 실행
기본적으로 상태 기반 테스트는 테스트 스위트를 실행할 때 포함됩니다. 이를 실행하기 위해서는 특별한 작업이 필요하지 않습니다.
--stateful
플래그를 사용하여 상태 저장 테스트를 제외하거나 상태 저장 테스트만 실행하도록 선택할 수 있습니다. 이 옵션은 지속적인 통합을 설정할 때 테스트 스위트를 분할하는 데 유용할 수 있습니다.
상태 기반 테스트만 실행하려면:
$ brownie test --stateful true
상태 기반 테스트를 건너 뛰려면:
$ brownie test --stateful false
상태 기반 테스트가 활성화되면 콘솔에 스피너가 표시되며, 테스트 실행이 완료될 때마다 회전합니다. 색상이 노랑에서 빨강으로 변경되면 테스트가 실패했음을 의미하며, Hypothesis는 이제 실패로 가는 가장 짧은 경로를 찾고 있습니다.
'블록체인 (Block Chain) > 이더리움' 카테고리의 다른 글
[브라우니 (Brownie)] 20. MythX를 사용한 보안 분석 (0) | 2023.06.18 |
---|---|
[브라우니 (Brownie)] 19. 커버리지 평가 (0) | 2023.06.18 |
[브라우니 (Brownie)] 17. 속성 기반 테스트 (0) | 2023.06.18 |
[브라우니 (Brownie)] 16. 픽스처 및 마커 참조 (1) | 2023.06.18 |
[브라우니 (Brownie)] 15. 단위 테스트 작성하기 (0) | 2023.06.18 |