이 장에서는 스마트 컨트랙트를 테스트하는 방법을 소개합니다. 블록체인의 트랜잭션은 되돌릴 수 없으므로 스마트 컨트랙트를 배포하기 전에 테스트하는 것이 매우 중요합니다.
트러플(Truffle)로 테스트하기
트러플은 자동 테스트 프레임워크를 제공합니다. 이 프레임워크를 사용하여 간단하고 관리 가능한 테스트를 작성할 수 있는 방법이 두 가지 있습니다.
Javascript 및 TypeScript를 이용하여 블록체인 외부에서 컨트랙트를 실행하는 애플리케이션처럼 작성
Solidity를 활용하여 컨트랙트 함수를 직접 호출
1) 시작하기
배포하기 전에 스마트 컨트랙트 테스트를 위해 값 설정 함수 setGreet 함수를 추가합니다. 소스 코드는 아래와 같습니다.
pragma solidity ^0.5.6;
contract Mortal {
/* 주소 타입의 소유자(owner) 변수 정의 */
address payable owner;
/* 이 함수는 초기화 시점에 실행되어 컨트랙트 소유자를 설정합니다 */
constructor () public { owner = msg.sender; }
/* 컨트랙트에서 자금을 회수하는 함수 */
function kill() public payable { if (msg.sender == owner) selfdestruct(owner); }
}
contract MEVerseGreeter is Mortal {
/* 문자열 타입의 변수 greeting 정의 */
string greeting;
/* 이 함수는 컨트랙트가 실행될 때 작동합니다 */
constructor (string memory _greeting) public {
greeting = _greeting;
}
/* 주(Main) 함수 */
function greet() public view returns (string memory) {
return greeting;
}
/* 테스트를 위해 새로 추가된 함수입니다 */
function setGreet(string memory _greeting) public {
// 소유자(owner)만 greeting 메세지를 수정할 수 있습니다
require(msg.sender == owner, "Only owner is allowed.");
greeting = _greeting;
}
}
테스트 시나리오
greet() 함수가 "Hello, MEVerse"이라는 메세지를 잘 출력하는지
setGreet() 함수가 새로 설정된 greeting 메세지를 잘 출력하고 소유자가 아닌 계정이 greeting을 업데이트하려고 할 때 revert를 하는지
먼저 일반적인 어설션(assertions)을 위해 Chai 어설션 라이브러리를 설치하고 (또는 사용하고 있는 다른 어설션 라이브러리도 괜찮습니다), 스마트 컨트랙트 어설션을 위해 truffle-assertions 라이브러리를 설치합니다.
npm install --save-dev chai truffle-assertions
2) 솔리디티로 테스트 작성하기
솔리디티로 테스트하는 것은 자바스크립트로 테스트하는 것보다 조금 더 직관적일 수 있습니다. 솔리디티 테스트 컨트랙트는 자바스크립트 테스트와 함께 .sol 파일로 제공됩니다.
test 폴더에 TestMEVerseGreeting.sol 이름의 파일을 생성합니다. 트러플 제품군은 테스트를 위한 헬퍼(helper) 라이브러리를 제공하므로, 이들을 불러옵니다. 솔리디티 테스트 예시를 살펴봅시다.
$ truffle test
Using network 'test'.
Compiling your contracts...
===========================
> Compiling ./test/TestMEVerseGreeting.sol
> Artifacts written to /var/folders/8k/pj9b6lld4qn4hq1l7h97y9pm0000gn/T/test--5158-mS50YzRSpyUZ
> Compiled successfully using:
- solc: 0.5.16+commit.9c3226ce.Emscripten.clang
TestMEVerseGreeting
1) testGreetingMessage
> No events were emitted
0 passing (3s)
1 failing
1) TestMEVerseGreeting
testGreetingMessage:
Error: greeting message should match (Tested: Hello, MEVerse, Against: Hello MEVerse)
at checkResultForFailure (/Users/meverselabs/.nvm/versions/node/v16.16.0/lib/node_modules/truffle/build/webpack:/packages/core/lib/testing/SolidityTest.js:66:1)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at Context.<anonymous> (/Users/meverselabs/.nvm/versions/node/v16.16.0/lib/node_modules/truffle/build/webpack:/packages/core/lib/testing/SolidityTest.js:92:1)
테스트를 실패했습니다.
Error: greeting message should match (Tested: Hello, MEVerse, Against: Hello MEVerse)
배포컨트랙트는 Hello, MEVerse이고 테스트로 작성한 코드는 Hello MEVerse ',(comma)' 로 동일하지 않아서 테스트 실패하였습니다. 테스트 코드 수정(추가)후 테스트 해보겠습니다.
$ truffle test
Using network 'test'.
Compiling your contracts...
===========================
> Compiling ./test/TestMEVerseGreeting.sol
> Artifacts written to /var/folders/8k/pj9b6lld4qn4hq1l7h97y9pm0000gn/T/test--5231-DBNS4Mgo7Mo0
> Compiled successfully using:
- solc: 0.5.16+commit.9c3226ce.Emscripten.clang
TestMEVerseGreeting
✓ testGreetingMessage (41ms)
1 passing (3s)
테스트가 성공적으로 통과 된 것을 확인합니다.
테스트넷에서 컨트랙트를 테스트하고 싶으면 network 옵션을 사용하면 됩니다.
$ truffle test -network MEVerseTestnet
Using network 'test'.
Compiling your contracts...
===========================
> Compiling ./contracts/MEVerseGreeter.sol
> Compiling ./test/TestMEVerseGreeting.sol
> Artifacts written to /var/folders/8k/pj9b6lld4qn4hq1l7h97y9pm0000gn/T/test--5324-jtF8M5Qz82cV
> Compiled successfully using:
- solc: 0.5.16+commit.9c3226ce.Emscripten.clang
TestMEVerseGreeting
✓ testGreetingMessage (55ms)
1 passing (3s)
3) 자바스크립트로 테스트 작성하기
트러플은 자바스크립트 테스트를 위한 견고한 프레임워크를 제공하기 위해 Mocha 테스트 프레임워크 및 Chai 어설션 라이브러리를 사용합니다. 자바스크립트 테스트는 더 많은 유연성을 제공하며 더 복잡한 테스트를 작성할 수 있게 합니다.
test 폴더에 0_MEVerseGreeting.js 이름의 파일을 생성하고 다음의 테스트 코드를 추가합니다.
// MEVerseGreeter 컨트랙트와 직접 상호작용
const MEVerseGreeter = artifacts.require("./MEVerseGreeter.sol");
const truffleAssert = require('truffle-assertions');
contract("MEVerseGreeter", async(accounts) => {
// 컨트랙트 인스턴스를 상위 레벨에 저장해
// 모든 함수에서 접근할 수 있도록 합니다.
var MEVerseGreeterInstance;
var owner = accounts[0];
var greetMsg = "Hello, MEVerse";
// 각 테스트가 진행되기 전에 실행됩니다.
before(async function() {
// set contract instance into a variable
MEVerseGreeterInstance = await MEVerseGreeter.new(greetMsg, {from:owner});
})
it("#1 check Greeting message", async function() {
// set the expected greeting message
var expectedGreeting = greetMsg;
var greet= await MEVerseGreeterInstance.greet();
assert.equal(expectedGreeting, greet, "greeting message should match");
})
it("#2 update greeting message.", async function() {
var newGreeting = "Hi, MEVerse";
await MEVerseGreeterInstance.setGreet(newGreeting, { from:owner });
var greet = await MEVerseGreeterInstance.greet();
assert.equal(newGreeting, greet, "greeting message should match");
});
it("#3 [Failure test] Only owner can change greeting.", async function() {
var fakeOwner = accounts[1];
await truffleAssert.fails(MEVerseGreeterInstance.setGreet(greetMsg, { from:fakeOwner }));
});
});
테스트 파일(0_MEVerseGreeting.js)을 지정하여 테스트를 실행합니다.
$ truffle test ./test/0_MEVerseGreeting.js
Using network 'test'.
Compiling your contracts...
===========================
> Compiling ./contracts/MEVerseGreeter.sol
> Artifacts written to /var/folders/8k/pj9b6lld4qn4hq1l7h97y9pm0000gn/T/test--5527-DYxpp0UInYnm
> Compiled successfully using:
- solc: 0.5.16+commit.9c3226ce.Emscripten.clang
Contract: MEVerseGreeter
✓ #1 check Greeting message
✓ #2 update greeting message. (42ms)
✓ #3 [Failure test] Only owner can change greeting. (176ms)
3 passing (301ms)