브라우니는 단위 테스트를 위해 pytest 프레임워크를 사용합니다. Pytest는 성숙하고 기능이 풍부한 테스트 프레임워크입니다. 최소한의 코드로 작은 테스트를 작성할 수 있으며 대규모 프로젝트에 확장성이 뛰어납니다.
테스트를 실행하려면:
$ brownie test
이 문서에서는 기본 pytest 사용 방법을 간단하게 설명하며, 특히 Brownie와 관련된 기능에 중점을 둡니다. pytest의 많은 구성 요소는 부분적으로 또는 전혀 설명되지 않았습니다. pytest에 대해 더 자세히 알고 싶다면 공식 pytest 문서를 참조해야합니다 (https://docs.pytest.org/en/latest/).
시작하기
테스트 파일 구조
Pytest는 프로젝트의 테스트 스위트에 포함해야하는 함수를 찾기 위해 test discovery 프로세스를 수행합니다.
- 테스트는 프로젝트의 tests/ 디렉토리 또는 하위 디렉토리에 저장되어야합니다.
- 파일 이름은 test_*.py 또는 *_test.py와 일치해야합니다.
테스트 파일 내에서 다음 메서드가 테스트로 실행됩니다.
- test로 시작하는 클래스 외 함수
- test로 시작하는 클래스 메서드 (클래스는 Test로 시작하고 init 메서드를 포함하지 않음)
첫 번째 테스트 작성하기
다음은 Brownie 및 pytest를 사용하여 계정 잔액이 거래를 수행한 후 올바르게 변경되었는지 확인하는 매우 간단한 테스트 예입니다.
from brownie import accounts
def test_account_balance():
balance = accounts[0].balance()
accounts[0].transfer(accounts[1], "10 ether", gas_price=0)
assert balance - "10 ether" == accounts[0].balance()
픽스처
픽스처는 하나 이상의 테스트 함수에 적용되는 함수로, 각 테스트 실행 전에 호출됩니다. 픽스처는 테스트에 필요한 초기 조건을 설정하는 데 사용됩니다.
픽스처는 @pytest.fixture
데코레이터를 사용하여 선언됩니다. 테스트에 픽스처를 전달하려면, 테스트의 입력 인수로 픽스처 이름을 포함하면 됩니다:
import pytest
from brownie import Token, accounts
@pytest.fixture
def token():
return accounts[0].deploy(Token, "Test Token", "TST", 18, 1000)
def test_transfer(token):
token.transfer(accounts[1], 100, {'from': accounts[0]})
assert token.balanceOf(accounts[0]) == 900
이 예제에서는 token
픽스처가 test_transfer
를 실행하기 전에 호출됩니다. 픽스처는 배포된 Contract
인스턴스를 반환하며 이것은 테스트에서 사용됩니다.
픽스처는 다른 픽스처의 종속성으로 포함될 수 있습니다:
import pytest
from brownie import Token, accounts
@pytest.fixture
def token():
return accounts[0].deploy(Token, "Test Token", "TST", 18, 1000)
@pytest.fixture
def distribute_tokens(token):
for i in range(1, 10):
token.transfer(accounts[i], 100, {'from': accounts[0]})
브라우니 Pytest 픽스처
브라우니는 프로젝트와의 상호 작용 및 테스트를 간소화하는 픽스처를 제공합니다. 대부분의 핵심 브라우니 기능은 임포트 문이 아닌 픽스처를 통해 액세스할 수 있습니다. 예를 들어, 다음은 임포트가 아닌 브라우니 픽스처를 사용한 이전 예제입니다:
import pytest
@pytest.fixture
def token(Token, accounts):
return accounts[0].deploy(Token, "Test Token", "TST", 18, 1000)
def test_transfer(token, accounts):
token.transfer(accounts[1], 100, {'from': accounts[0]})
assert token.balanceOf(accounts[0]) == 900
모든 사용 가능한 픽스처에 대한 정보는 Fixture and Marker Reference를 참조하십시오.
픽스처 범위
픽스처의 기본 동작은 각 테스트에서 필요할 때마다 실행하는 것입니다. 데코레이터에 scope
매개변수를 추가하여 픽스처 실행 빈도를 변경할 수 있습니다. 범위에 대한 가능한 값은 function
, class
, module
, 또는 session
입니다.
예시를 더 확장해보겠습니다:
import pytest
@pytest.fixture(scope="module")
def token(Token):
return accounts[0].deploy(Token, "Test Token", "TST", 18, 1000)
def test_approval(token, accounts):
token.approve(accounts[1], 500, {'from': accounts[0]})
assert token.allowance(accounts[0], accounts[1]) == 500
def test_transfer(token, accounts):
token.transfer(accounts[1], 100, {'from': accounts[0]})
assert token.balanceOf(accounts[0]) == 900
토큰 픽스처에 module 스코프를 적용함으로써, 계약은 한 번만 배포되며 test_approval
및 test_transfer
테스트 모두에 동일한 Contract
인스턴스가 사용됩니다.
session
또는 module
과 같이 높은 스코프의 픽스처는 낮은 스코프의 픽스처(예: function
)보다 항상 먼저 인스턴스화됩니다. 동일한 스코프의 픽스처의 상대적인 순서는 테스트 함수에서 선언된 순서를 따르며, 픽스처 간의 의존성을 준수합니다. 이 규칙의 유일한 예외는 아래에서 설명하는 isolation 픽스처입니다.
격리 픽스처
많은 경우 테스트 환경을 재설정하여 테스트 간에 격리시키고자 할 수 있습니다. 격리되지 않으면 테스트 결과가 이전 테스트에서 수행된 작업에 의존할 수 있습니다.
Brownie는 isolation을 처리하기 위해 두 개의 픽스처를 제공합니다.
module_isolation
은 모듈 스코프 픽스처입니다. 이는 모듈을 완료하기 전에 로컬 체인을 재설정하여 이 모듈을 위한 깨끗한 환경을 보장하고, 이를 통해 결과가 다음 모듈에 영향을 미치지 않도록 합니다.
fn_isolation
은 함수 스코프입니다. 이는 각 테스트를 실행하기 전에 체인의 스냅샷을 추가로 가져와 테스트가 완료되면 스냅샷으로 되돌립니다. 이를 통해 각 테스트에 대해 공통 상태를 정의하여 반복적인 트랜잭션을 줄일 수 있습니다.
격리 픽스처는 항상 해당 범위 내에서 실행되는 첫 번째 픽스처입니다. 함수 스코프 픽스처 내에서 수행된 모든 작업이 격리 스냅샷 이후에 수행된다는 것을 보장할 수 있습니다.
모든 테스트에 격리 픽스처를 적용하려면 다른 픽스처에서 가져와 autouse
매개변수를 포함하면 됩니다:
import pytest
@pytest.fixture(scope="module", autouse=True)
def shared_setup(module_isolation):
pass
이 픽스처를 여러 모듈에 적용하려면 conftest.py 파일에 배치할 수도 있습니다.
공유 초기 상태 정의
일반적으로, 초기 테스트 조건을 정의하는 하나 이상의 모듈 범위 설정 픽스처를 포함하고, 그 다음 fn_isolation
를 사용하여 각 테스트 시작 시 기본 상태로 되돌립니다. 예를 들면:
import pytest
@pytest.fixture(scope="module", autouse=True)
def token(Token, accounts):
t = accounts[0].deploy(Token, "Test Token", "TST", 18, 1000)
yield t
@pytest.fixture(autouse=True)
def isolation(fn_isolation):
pass
def test_transfer(token, accounts):
token.transfer(accounts[1], 100, {'from': accounts[0]})
assert token.balanceOf(accounts[0]) == 900
def test_chain_reverted(token):
assert token.balanceOf(accounts[0]) == 1000
위 예제의 이벤트 순서는 다음과 같습니다:
1. module_isolation
의 설정 단계가 실행되어 로컬 환경이 재설정됩니다. 2. 모듈 범위의 token
fixture이 실행되어 총 발행량이 1000 토큰인 Token
컨트랙트가 배포됩니다. 3. 함수 범위의 fn_isolation
fixture의 설정 단계가 실행됩니다. 블록체인의 스냅샷이 촬영됩니다. 4. test_transfer
가 실행되어 100 토큰이 accounts[0]
에서 accounts[1]
로 이전됩니다. 5. 함수 범위의 fn_isolation
fixture의 해제 단계가 실행됩니다. 블록체인은 test_transfer
전의 상태로 되돌아갑니다. 6. 함수 범위의 fn_isolation
fixture의 설정 단계가 다시 실행됩니다. 이전 스냅샷과 동일한 스냅샷이 촬영됩니다. 7. test_chain_reverted
가 실행됩니다. fn_isolation
fixture 인해 어서트 문이 통과합니다. 8. 함수 범위의 fn_isolation
fixture의 해제 단계가 실행됩니다. 블록체인은 test_chain_reverted
전의 상태로 되돌아갑니다. 9. 모듈 범위의 module_isolation
fixture의 해제 단계가 실행되어 로컬 환경이 재설정됩니다.
마커
마커(marker)는 테스트 함수에 적용되는 데코레이터입니다. 마커는 픽스처와 플러그인에서 접근 가능한 테스트에 대한 메타 데이터를 전달하기 위해 사용됩니다.
특정 테스트에 마커를 적용하려면 @pytest.mark
데코레이터를 사용하세요:
@pytest.mark.foo
def test_with_example_marker():
pass
모듈 레벨에서 마커를 적용하려면 pytestmark
전역 변수를 추가하십시오:
import pytest
pytestmark = [pytest.mark.foo, pytest.mark.bar]
표준 pytest markers와 함께, Brownie는 스마트 컨트랙트 테스트에 특화된 추가적인 마커를 제공합니다. 자세한 내용은 문서의 markers reference 섹션을 참조하세요.
트랜잭션 실패 처리
테스트를 실행할 때, 실패하는 트랜잭션은 VirtualMachineError
예외를 발생시킵니다. 이를 위해 당신은 brownie.reverts
를 컨텍스트 매니저로 사용하여 어설션을 작성할 수 있습니다. 이는 pytest.raises
와 매우 유사하게 작동합니다.
import brownie
def test_transfer_reverts(accounts, Token):
token = accounts[0].deploy(Token, "Test Token", "TST", 18, 1e23)
with brownie.reverts():
token.transfer(accounts[1], 1e24, {'from': accounts[0]})
선택적으로 문자열을 인자로 포함할 수 있습니다. 만약 문자열이 주어진다면, 거래에서 반환된 오류 문자열은 테스트를 통과하기 위해 일치해야합니다.
import brownie
def test_transfer_reverts(accounts, Token):
token = accounts[0].deploy(Token, "Test Token", "TST", 18, 1e23)
with brownie.reverts("Insufficient Balance"):
token.transfer(accounts[1], 1e24, {'from': accounts[0]})
개발자 Revert Comments
각 revert 문자열은 계약 배포 비용에 최소 20000 가스를 추가하며 함수 실행 비용을 증가시킵니다. 모든 require
및 revert
문에 revert 문자열을 포함하는 것은 종종 비실용적이며 때로는 블록 가스 제한으로 인해 불가능합니다.
이러한 이유로 Brownie는 바이트 코드에 포함되지 않지만 TransactionReceipt.revert_msg
를 통해 여전히 액세스 할 수 있는 소스 코드 주석으로 revert 문자열을 포함할 수 있습니다. 당신은 가스 비용을 증가시키지 않고 특정 require
또는 revert
문을 대상으로하는 테스트를 작성할 수 있습니다.
Solidity에서 Revert 문자열 주석은 // dev:
로 시작해야하며 Vyper에서는 # dev:
로 시작해야합니다. 컴파일된 revert 문자열에 항상 우선순위가 부여됩니다. 몇 가지 예시:
function revertExamples(uint a) external {
require(a != 2, "is two");
require(a != 3); // dev: is three
require(a != 4, "cannot be four"); // dev: is four
require(a != 5); // is five
}
- 라인 2는 주어진 반환 문자열 "is two"를 사용합니다.
- 라인 3은 코멘트에서 제공된 문자열 "dev: is three"을 대체합니다.
- 라인 4는 주어진 문자열 "cannot be four"를 사용하고 대체 문자열을 무시합니다.
- 라인 5에는 반환 문자열이 없습니다. 코멘트가 "dev:"로 시작하지 않았으므로 무시됩니다.
위의 함수가 콘솔에서 실행되면:
>>> tx = test.revertExamples(3)
Transaction sent: 0xd31c1c8db46a5bf2d3be822778c767e1b12e0257152fcc14dcf7e4a942793cb4
test.revertExamples confirmed (dev: is three) - block: 2 gas used: 31337 (6.66%)
<Transaction object '0xd31c1c8db46a5bf2d3be822778c767e1b12e0257152fcc14dcf7e4a942793cb4'>
>>> tx.revert_msg
'dev: is three'
코드에 오류 문자열이 포함되어 있어도, TransactionReceipt.dev_revert_msg
를 통해 여전히 개발자가 되돌린 이유에 접근할 수 있습니다:
>>> tx = test.revertExamples(4)
Transaction sent: 0xd9e0fb1bd6532f6aec972fc8aef806a8d8b894349cf5c82c487335625db8d0ef
test.revertExamples confirmed (cannot be four) - block: 3 gas used: 31337 (6.66%)
<Transaction object '0xd9e0fb1bd6532f6aec972fc8aef806a8d8b894349cf5c82c487335625db8d0ef'>
>>> tx.revert_msg
'cannot be four'
>>> tx.dev_revert_msg
'dev: is four'
테스트의 매개 변수화
@pytest.mark.parametrize
마커를 사용하면 테스트 함수의 인수를 매개 변수화 할 수 있습니다. 다음은 특정 입력이 예상 출력으로 이어지는지 확인하는 매개 변수화된 테스트 함수의 전형적인 예입니다.
import pytest
@pytest.mark.parametrize('amount', [0, 100, 500])
def test_transferFrom_reverts(token, accounts, amount):
token.approve(accounts[1], amount, {'from': accounts[0]})
assert token.allowance(accounts[0], accounts[1]) == amount
예제에서 @parametrize
데코레이터는 amount
에 대해 세 가지 다른 값을 정의합니다. test_transferFrom_reverts
함수는 각각을 차례로 사용하여 세 번 실행됩니다.
@given
데코레이터를 사용하여 정의된 범위에서 자동으로 매개 변수화된 테스트를 생성할 수 있습니다.
from brownie.test import given, strategy
@given(amount=strategy('uint', max_value=1000))
def test_transferFrom_reverts(token, accounts, amount):
token.approve(accounts[1], amount, {'from': accounts[0]})
assert token.allowance(accounts[0], accounts[1]) == amount
이 기술은 속성 기반 테스트라고합니다. 자세한 내용은 속성 기반 테스트를 읽어보십시오.
다른 프로젝트에 대한 테스트
pm
픽스처는 브라우니 패키지 관리자로 설치된 패키지에 액세스 할 수 있습니다. 이 픽스처를 사용하여 프로젝트와 다른 프로젝트 간의 상호 작용을 확인하는 테스트 케이스를 작성할 수 있습니다.
pm
은 프로젝트 ID를 인수로 받아 Project
객체를 반환하는 함수입니다. 이렇게하면 패키지에서 계약을 배포하고 이를 픽스처로 전달하여 테스트에서 사용할 수 있습니다:
@pytest.fixture(scope="module")
def compound(pm, accounts):
ctoken = pm('defi.snakecharmers.eth/compound@1.1.0').CToken
yield ctoken.deploy({'from': accounts[0]})
프로젝트 의존성 목록에 필요한 테스트 패키지를 추가해야합니다.
테스트 실행
전체 테스트 스위트를 실행하려면:
$ brownie test
특정 테스트를 실행하려면:
$ brownie test tests/test_transfer.py
테스트 결과는 build/tests.json
에 저장됩니다. 이 파일에는 각 테스트의 결과, 커버리지 분석 데이터 및 테스트가 마지막으로 실행된 이후 관련 파일이 변경되었는지 확인하는 데 사용되는 해시가 저장됩니다. KeyboardInterrupt
를 통해 테스트 실행을 중단하는 경우, 완전히 완료된 모듈에 대해서만 결과가 저장됩니다.
업데이트된 테스트만 실행하기
테스트 스위트가 한 번 실행된 후에는 --update
플래그를 사용하여 변경 사항이 발생한 테스트만 반복할 수 있습니다:
$ brownie test --update
모듈은 이렇게 건너뛰기 위해 모든 테스트 함수에서 module_isolation
또는 fn_isolation
fixture를 사용해야 합니다.
pytest
콘솔 출력에서 건너뛴 테스트는 s
로 나타나지만, 마지막으로 실행될 때 테스트가 통과했는지 표시하기 위해 녹색 또는 빨간색으로 색칠됩니다.
커버리지 분석도 활성화되어 있으면, 이전에 완료된 분석하지 않은 테스트가 다시 실행됩니다. 최종 커버리지 보고서에는 건너뛴 모듈의 결과가 포함됩니다.
Brownie는 다음 항목들의 해시를 비교하여 테스트를 다시 실행해야 할지 여부를 확인합니다:
- 테스트 실행 중 배포된 모든 컨트랙트의 바이트코드
- 테스트 모듈의 AST
- 테스트 모듈에 액세스할 수 있는 모든
conftest.py
모듈의 AST
대화형 디버깅
-interactive
플래그를 사용하면 테스트를 실행하는 동안 프로젝트를 디버그할 수 있습니다:
$ brownie test --interactive
인터랙티브 모드를 사용할 때, Brownie는 각 실패한 테스트에 대한 트레이스백을 즉시 출력하고 콘솔을 엽니다. 배포된 컨트랙트와 트랜잭션 기록을 검토하여 무엇이 잘못되었는지를 확인할 수 있습니다.
- 배포된
ProjectContract
객체는 해당하는ContractContainer
에서 사용 가능합니다.
TransactionReceipt
객체는TxHistory
컨테이너에 있으며,history
로 사용 가능합니다.
- 최근 트랜잭션을 되돌아보고 전진하기 위해
chain.undo
와chain.redo
를 사용합니다.
작업을 완료하면 quit()
를 입력하여 다음 테스트를 진행합니다.
Brownie의 디버깅 기능에 대한 자세한 내용은 트랜잭션 검토 및 디버깅을 참조하십시오.
가스 사용 평가
가스 프로파일 보고서를 생성하려면 --gas
플래그를 추가하십시오:
$ brownie test --gas
테스트가 완료되면 보고서가 표시됩니다:
Gas Profile:
Token <Contract>
├─ constructor - avg: 1099591 low: 1099591 high: 1099591
├─ transfer - avg: 43017 low: 43017 high: 43017
└─ approve - avg: 21437 low: 21437 high: 21437
Storage <Contract>
├─ constructor - avg: 211445 low: 211445 high: 211445
└─ set - avg: 21658 low: 21658 high: 21658
커버리지 평가
단위 테스트 커버리지를 확인하려면, --coverage
플래그를 추가하세요.:
$ brownie test --coverage
테스트가 완료되면 보고서가 표시됩니다:
contract: Token - 80.8%
Token.allowance - 100.0%
Token.approve - 100.0%
Token.balanceOf - 100.0%
Token.transfer - 100.0%
Token.transferFrom - 100.0%
SafeMath.add - 75.0%
SafeMath.sub - 75.0%
Token.<fallback> - 0.0%
Coverage report saved at reports/coverage.json
브라우니는 각 계약 방법에 대한 % 점수를 출력하여 전반적인 커버리지 수준을 빠르게 평가할 수 있습니다. 자세한 커버리지 보고서는 프로젝트의 reports
폴더에 저장되며, 브라우니 GUI를 통해 볼 수 있습니다. 자세한 정보는 보고서 보기를 참조하세요.
프로젝트의 구성 파일을 수정하여이 보고서에서 특정 계약 또는 소스 파일을 제외 할 수 있습니다.
분산 테스트를 위해 xdist
사용하기
Brownie는 pytest-xdist 플러그인과 호환되어 테스트 실행을 병렬화 할 수 있습니다. 대규모 테스트 스위트의 경우 전체 실행 시간을 크게 단축시킬 수 있습니다.
플러그인을 사용하기 전에 xdist 작동 방식에 대한 개요를 읽어 보는 것이 좋습니다.
테스트를 병렬로 실행하려면 -n
플래그를 포함하십시오:
$ brownie test -n auto
테스트는 모듈 단위로 작업자에게 배포됩니다. 실행되는 모든 테스트에 대해 격리 픽스처를 적용해야합니다. 그렇지 않으면 xdist
수집 후 실패합니다. 적절한 격리가 없으면 테스트 실행 간에 일관된 동작을 보장할 수 없기 때문입니다.
'블록체인 (Block Chain) > 이더리움' 카테고리의 다른 글
[브라우니 (Brownie)] 17. 속성 기반 테스트 (0) | 2023.06.18 |
---|---|
[브라우니 (Brownie)] 16. 픽스처 및 마커 참조 (1) | 2023.06.18 |
[브라우니 (Brownie)] 14. 데이터 유형 (0) | 2023.06.16 |
[브라우니 (Brownie)] 13. 트랜잭션 검사 및 디버깅하기 (0) | 2023.06.16 |
[브라우니 (Brownie)] 12. 블록체인과 상호작용하기 (0) | 2023.06.16 |