[DreamChain DApp] #8 Smart Contract Unit Test 1

in #kr6 years ago (edited)

본격적으로 스마트 컨트랙트 기능을 테스트해보겠습니다.

이전글 - [DreamChain DApp] #7 Unit Test 환경구축 2

본 내용은 Ethereum and Solidity: The Complete Developer's Guide을 참고해서 작성되었습니다.


스마트 컨트랙트의 기능을 테스트한다고 했는데, 무엇을 테스트해야 할까 막막합니다. 그러나 테스트는 누가 정해주지 않습니다. 개발자가 직접 다양한 상황을 고려해서 되도록 많은 것을 테스트 해야 합니다. 다음과 같은 것들이 있을 수 있습니다.

  • 컨트랙트가 배포되어 주소가 존재하는가?
  • 컨트랙트를 생성할 때 초기값은 제대로 저장되는가?
  • contribute 함수가 호출되면 컨트랙트의 balance가 증가하는가?
  • approvalWithdrawal 함수가 호출되면 approvals_count가 증가하는가?
  • finalizeWithdrawal 함수가 호출되면 author로 금액이 송금되는가?

이전에 Remix로 대부분 테스트 한 내용입니다. 여기서는 mocha 테스트 프레임워크를 사용해서 테스트 해보겠습니다.

1. Deployed Contract Address

이전글에서 테스트 환경을 구축하고, 가장 먼저 deploy된 smart contract object를 출력하는 테스트를 실행했었습니다. 사실 이건 테스트는 아니고, 테스트가 어떻게 돌아가는지만 보려고 한 것입니다. 아래 코드를 보면 두번째 테스트로 deploy된 컨트랙트의 주소가 존재하는지를 검사하는 테스트입니다. 당연히 deploy됐으면 주소가 존재해야 합니다.

// test groups
describe( 'DreamStory', () => {
    // unit test: simply print out the deployed contract object
    it( 'Deploy a DreamStory contract', () => {
      // print out deployed contract object
      console.log( dream_story );
    });
    // unit test: contract address exists
    it( 'Deployed contract address', () => {
      // use assert.ok function to check if the address exists
      assert.ok( dream_story.options.address );
      // print out the address
      console.log( dream_story.options.address );
    });
});
  • assert.ok 함수는 인자가 존재하면 true를 리턴함.
  • deploy된 컨트랙트는 options 객체를 가지고, options객체는 address 변수를 가짐.

아래와 같이 테스트를 실행시킵니다.

$ npm run tess
  • 테스트 결과
  • deploy된 주소가 콘솔에 표시됨. 0x4f84D3C670F443feb9D986f86AB9862D1ceE2176.

2. Minimum Download Price in Wei

DreamStory 컨트랙트를 배포할 때, Minimum Download Price in Wei라는 값을 넣게 되어 있습니다. author가 지정하는 값으로, contributor가 dream story를 다운로드 할 때, 최소한으로 지불해야 하는 금액을 설정하는 것입니다. 테스트 코드에서 100 wei로 설정한 하고 컨트랙트 배포한 후, 배포된 컨트랙트에 접속하여 값을 읽어보는 테스트를 진행하겠습니다.

컨트랙트 생성시입력할 값을 변수 INIT_MIN_DOWN_PRICE로 다음과 같이 설정합니다. 그리고 beforeEach 내용도 이전글에서 약간 변경되었습니다. 컨트랙트 deploy함수의 인자로 위 변수를 대입했습니다.

테스트 코드는 새롭게 추가되는 부분만 표시하겠습니다. 위 내용을 이전글에 소개한 테스트 블락 내에 코딩해야 합니다.

// unit test: check if the minimum download price is the same as the input
    it( 'Check minimum download price', async () => {
      // get the minimum_down_price_wei of the contract
      // this uses await since it needs to connect the Contract
      // to get the value of state variable, need to use call() function
      const min_down_price_wei= await dream_story.methods.min_down_price_wei().call();
      // check if the state variable min_down_price_wei is equal to the intial value
      assert.equal( min_down_price_wei, INIT_MIN_DONW_PRICE );
      // print out the min_down_price_wei
      console.log( min_down_price_wei );
    })
  • 컨트랙트에 접속하기 때문에 asyn 함수로 선언하고, await로 함수 호출 결과를 기다림.
  • 컨트랙트의 상태변수는 기본적으로 값을 리턴하는 함수가 만들어짐. 여기서는 min_down_price_wei()
  • 위처럼 함수 호출하고 끝나는게 아니라 .call() 함수를 호출해야만 상태 변수를 읽어옴
  • 참고로, call()함수는 gas를 소모하지 않음. 반면에 상태 변수를 바꾸는 .send()함수의 경우는 gas 소모가 필요함.
  • assert.equal 함수는 먼저 테스트 하고자 하는 변수를 쓰고, 그 변수가 가져야할 값을 씀. 두 개의 값이 동일하면 true를 리턴
  • 테스트 결과
  • 컨트랙트 배포할 때 입력한 100 wei가 제대로 설정됨.

3. contribute 함수 테스트

[컨트랙트 코드 변경]

테스트 작업을 하다보니 빠진 부분을 발견했습니다. contribute 함수를 수행할 때 입력한 금액이 0보다 큰 경우만 contributor로 받아들이는 코드를 추가하여 아래와 같이 변경해주세요.

/*
     * A contributor donates some money for a dream story.
     * So the money will be transfered to this contract address, resulted in increasing the balance
     * @note this function can receive some money, which is msg.value
     */
    function contribute() public payable {
        // check if the money is greater than zero
        require( msg.value > 0 );
        // increase the vote counts
        votes_count++;
        // set contributor address to true
        contributors[ msg.sender ]= true;
    }

컨트랙트 코드를 변경하면 반드시 compile.js 스크립트를 실행해야 합니다. 아래와 같이 compile.js가 위치한 곳을 이동하여 스크립트를 실행합니다.

$ cd ethereum
$ node compile.js

contribute 함수를 테스트 하기 위해 별도의 테스트 블락을 만듭니다. 굳이 별도의 블락을 만들지 않아도 되는데, 그룹화하면 테스트 구별하기도 좋고, 충분한 테스트가 되었는지 살펴보기도 좋습니다.
여기서 유저가 contribute 함수를 호출했을 때, 테스트 할 것은 세 가지 입니다.

  • votes_count가 증가하는가?
  • 유저는 contributor list에 등록되는가?
  • contribute한 금액이 컨트랙트의 balance로 송금되는가?

아래와 같이 새롭게 테스트 그룹을 만듭니다.

// test contribution value in ether
const INIT_CONTRIBUTE_ETH= '1';
// test groups: contribute tests
describe( 'DreamStory contribute function tests', () => {
  // unit test: check if the votes_count increase when a user contributes
  //            now the user should be inserted to contributor list
  //            the balance of the contract should increase
  it( 'Check votes count and others', async () => {
    // get the intial balance of the contract to test balance, which is in string
    let init_balance= await web3.eth.getBalance( dream_story.options.address );
    // convert the wei into ether
    init_balance= web3.utils.fromWei( init_balance, 'ether' );
    // cast the string to float
    init_balance= parseFloat( init_balance );
    // use the second account to contribute
    await dream_story.methods.contribute().send( {
      from: accounts[1],
      // amount to contribute. convert the ether into wei
      value: web3.utils.toWei( INIT_CONTRIBUTE_ETH, 'ether' ),
      // set gas limit`
      gas: '1000000'
    });
    // get the votes_count of the deployed contract
    const votes_count= await dream_story.methods.votes_count().call();
    // check if the votes_count increased
    assert.equal( votes_count, 1 );
    // print out the votes_count
    console.log( votes_count );
    // get the mapping value of the user account
    const is_contributor= await dream_story.methods.contributors( accounts[1] ).call();
    // assert the result
    assert( is_contributor );
    console.log( is_contributor );
    // get the balance of the author now. note that the balance is in string
    let balance= await web3.eth.getBalance( dream_story.options.address );
    // first convert the balance into ether
    balance= web3.utils.fromWei( balance, 'ether' );
    // cast the string to float
    balance= parseFloat( balance );
    console.log( init_balance );
    console.log( balance );
    // check if it is increased
    assert( balance > init_balance );
  })
});
  • contribution을 하기 전에 스마트 컨트랙트의 balance를 읽어서 저장
  • accounts[1]의 계정을 이용하여 contribution 금액을 200 wei로 하여 contribute 수행
  • contribution 후에 컨트랙트의 balance를 읽어서 별도의 변수에 저장
  • 테스트1: contributor의 수를 나타내는 votes_count가 1이되는지 확인
  • 테스트2: accounts[1]이 contributor인지 확인
  • 테스트3: contribution 후에 컨트랙트 balance가 증가하는지 확인
  • 테스트 결과
    • 첫번째로 나타나는 '1'은 votes_count 값
    • 두번째로 나타나는 'true'는 accounts[1]이 contributor인지 나타내는 것
    • 세번째와 네번째 숫자는 각각 contribution 전후의 스마트 컨트랙트 balance 값. 컨트랙트이 balance가 0에서 200 wei로 증가
    • assert( balance > init_balance+INIT_CONTRIBUTE_WEI-1 ); 처럼 부등호로 테스트 한 이유는 숫자의 등호 검증은 숫자가 표시되는 형식에 따라 다를 수 있기 때문. 특히나 소수점을 비교할 때는 특히나 등호 대신 부등호 사용 필요함

오늘도 분량상 여기까지 진행하고 다음에 다른 함수들도 제대로 동작하는지 테스트 해보겠습니다. 저도 글을 쓰면서 컨트랙트 코드를 조금씩 수정하면서 테스트 통과시키고 있습니다. 이렇게 진행하니 코드가 더욱 더 깔끔하고 보기 좋네요~
뭔놈의 테스트가 이리도 많느냐고 하실 수 있는데, 테스트는 코드 자체보다 더 중요한 단계이니 너그럽게 봐주세요~

오늘의 실습: contribute 함수에서 빼먹은 테스트는 뭐가 있을까요?