Unit Test 환경 구축

보다 체계적인 테스트를 위한 환경을 구축해 보겠습니다. 이와 같은 테스트를 Unit Test라고 하는데, 각 기능 유닛들을 테스트 한다는 의미입니다.

Remix로 테스트하면 되지 왜 별도의 테스트 환경을 구축하는지 의아할 수 있습니다. 코딩한 컨트랙트를 보다 체계적으로 테스트하고 테스트를 자동으로 하는 것은 코딩에 필수적입니다. 그러나 종종 테스트를 철저히 하지 않는 경우가 있습니다. 여기서는 코딩에 반드시 필요한 테스트 환경을 구축하는 것을 알아보겠습니다.

개발 환경은 우분투 16.04입니다. 윈도Windows와 조금 차이 있을수 있지만 큰 흐름은 같습니다.


  • 스마트 컨트랙트를 자동으로 컴파일하는 스크립트 작성
  • 컴파일 결과 Bytecode와 ABI 얻어짐
  • Bytecode를 이용하여 컨트랙트를 로컬 테스트 네트워크에 Deploy
  • ABI와 Web3를 이용하여 deploy된 컨트랙트의 내용 접근하여 기능 테스트 진행


1. npm 설정

npm은 자바스크립트 프로그래밍 모듈 관리자입니다. 특히 Node.js 개발 환경의 기본 모듈 관리자입니다. 자세한 내용은 구글링을 해보세요. npm이 설치돼 있지 않으면 설치해야 합니다.

$ curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
$ sudo apt-get install nodejs
$ sudo apt-get install build-essential


자세한 내용은 다음 사이트를 참고하세요.
https://twpower.github.io/100-install-nodejs-on-ubuntu


1.1 npm init

프로젝트 디렉토리의 최상위에서 다음 명령을 입력합니다.

$ npm init


실행하면 다음과 같이 몇 가지 물어보는게 그냥 엔터키만 누릅니다. 그러면 설정이 맞냐고 최종적으로 물어보는데 다시 한번 엔터키를 누르면 패키지 관련 파일인 package.json 파일이 생깁니다.



1.2 npm 패키지 인스톨

테스트를 진행하기 위해서는 다음과 같은 npm 패키지가 필요합니다.

  • ganache-cli: 로컬 이더리움 테스트 네트워크
  • mocha: 테스트 프레임워크
  • solc: 솔리디티 컴파일러
  • fs-extra: 파일 시스템 확장 기능
  • web3@1.0.0-beta.26: web3. 버전이 빠르게 업데이트되어 같은 환경을 맞추기 위함


$ npm install --save ganache-cli mocha solc fs-extra web3@1.0.0-beta.26

이 명령에 사용된 옵션 --save는 설치하는 모듈을 package.json파일에 저장하라는 의미입니다.

패키지 설치가 끝나면 다음과 같이 node_modules이라는 것이 생깁니다. 참고로, 저는 대략적인 디렉터리 구성을 해놓은 상태라 조금 다르게 보일 것입니다. node_modules에 위에서 설치한 모듈이 있어야 합니다. 앞서 설치한 것 이외에 의존되는 모듈이 더 많을 것입니다.



2. 디텍터리(폴더) 구성

컨트랙트 소스 파일과 테스트 파일, 웹페이지 파일(향후 추가) 등을 적절한 디렉터리로 구분해 주면 깔끔합니다. 다음과 같은 구조를 추천합니다.

compile.js, deploy.js, web3.js 등은 아직 설명을 안했지만 이와 같은 구조에 두면 좋습니다. 그리고 편의상 node_modules은 표시하지 않았습니다.


3. 자동 컴파일 스크립트 만들기

스마트 컨트랙트를 테스트 프로그램이 자동으로 빌드, 배포하려면 컨트랙트 컴파일을 자동으로 할 수 있어야 합니다. 이를 위해 다음과 같은 스크립트를 구성합니다.

  • 파일이름: compile.js
  • 파일위치: /ethereum/

// @note: this compile.js will be usually executed once unless the contract file is changed.
// path module to read soliditi files
const path= require('path');
// solidity compiler module
const solc= require('solc');
// file system extra functionality
const fs= require('fs-extra');

// get the build directory using the path module
// build directory is used to re-use the compiled code when no change occurs
// __dirname will indicate the root directory where the oxmpile.js occurs
const build_path= path.resolve( __dirname, 'build' );
// delete the build directory
fs.removeSync( build_path );

// get the contract file path
const contract_path= path.resolve( __dirname, 'contracts', 'DreamStory.sol' );
// read the contract file
const contract_src= fs.readFileSync( contract_path, 'utf8' );
// compiled output, extract only contracts part
const compile_output= solc.compile( contract_src, 1 ).contracts;
// create the build directory
fs.ensureDirSync( build_path );

// create build files looping over contracts
for( let contract in compile_output ) {
  // create a contract json file
  fs.outputJsonSync(
    path.resolve( build_path, contract.replace(':', '') + '.json' ),
    compile_output[ contract ]
  );
}

스크립트의 주요 기능은 컨트랙트 파일이 변경되면 build 디렉터리를 지우고, 새로 컴파일해서 그 결과 중에 contracts 부분만 추출하여 build 디렉터리 밑의 별도의 json 파일을 만드는 것입니다. 주의깊게 볼 점은 이 compile.js 파일은 컨트랙트 내용이 변경되지 않으면 다시 실행되지 않아도 됩니다. 그러나 컨트랙트 내용이 변경되면 반드시 다시 실행해야 합니다.

참고로 이 코드에서 한 파일에 컨트랙트가 여러 개일 경우에 컴파일 결과를 컨트랙트별로 별도의 파일로 저장합니다. 나중에 컨트랙트를 하나 추가할 것이기 때문에 여기서는 복수의 컨트랙트가 있는 상황에 맞게 코딩했습니다.


3.1 컴파일 스크립트 실행

그러면 컴파일 스크립트를 실행해 보겠습니다. 다음과 같이 compile.js가 있는 폴더로 이동한 후 스크립트를 실행합니다.

$ cd ethereum
$ node compile.js


문제없이 컴파일이 되면 ethereum/build 폴더 밑에 DreamStory.json 파일이 생길 것입니다.



4. 로컬 테스트 네트워크와 Web3

이번에는 컴파일된 bytecode를 로컬 테스트 네트워크에 deploy하고 컨트랙트 기능을 테스트하는 테스트 파일을 설명하겠습니다. deploy를 자동으로 하는 스크립트는 좀 나중으로 미루겠습니다. 이때는 로컬 네트워크에 deploy하지 않고, Rinkby와 같은 테스트 네트워크에 deploy할 것입니다. 이때 몇 가지 개념적으로 알아야 할 게 있어서 나중으로 미룹니다.


로컬 이더리움 네트워크 Ganache에 deploy된 컨트랙트에 접속하기 위해서는 몇 가지가 필요합니다.

이 그림을 보면, Web3라는 constructor 함수가 있어서 이것을 객체화instantiation하면 web3라는 객체가 만들어집니다. 이 web3로 로컬 테스트 네크워크(Ganache)에 접속하려면 그림처럼 Provider가 필요합니다. 이것은 네트워크와 web3를 연결시켜 주는 연결 통로Communication Layer입니다. 이 Provider는 테스트 네트워크에 따라 달라집니다. 그래서 그림에서도 web3 인스턴스에 서로 다른 Provider가 붙을 수 있다고 표시되어 있습니다. 나중에 Rinkby 테스트 네트워크와 연결할 때는 다른 Provider가 web3와 붙을 것입니다.

참고로, 로컬 테스트 네트워크 Ganache를 사용하면 Remix의 Javascript VM 환경처럼 일정 금액의 이더가 들어있는 여러 개의 이더리움 계정이 만들어 집니다. 따라서 로컬 네트워크를 별도로 세팅하지 않아도 되니 테스트에 매우 적합합니다. 또한 계정은 개인키 설정이 필요 없는 unlock 상태로 제공됩니다.

개념적으로 어려울 수도 있으니, 그냥 그런가 보다 하고 넘어가셔도 좋습니다.


5. 테스트 파일


테스트 파일은 이 구조 중 test 디텍터리에 생성합니다.

  • 파일위치: test
  • 파일이름: dreamstory.test.js


6. Mocha 테스트 Framework

Mocha라는 테스트 framework이 있습니다. 여기서는 스마트 컨트랙트의 기능을 테스트하기 위해서 Mocha를 사용합니다. Mocha에서 주로 사용하는 함수와 목적은 다음과 같습니다. 저도 Mocha를 처음 사용하지만, 개념적으로 보면 구글 C++ unittest의 Test, Fixture와 유사합니다.



Mocha에서는 'it'함수로 개별적 테스트를 진행하고, 이 'it'을 모아서 그룹핑하는 것이 'describe'입니다. 그리고 테스트를 수행하기 전에 사전에 수행되어야 하는 코드를 'beforeEach'라는 함수에서 처리하도록 합니다.

Mocha를 이용하려면 package.json 파일에서 다음과 같이 "test" 부분을 "Mocha"로 바꿔야 합니다.



7. 테스트 스크립트

그럼 간단히 테스트를 실행할 수 있는 스크립트를 작성해 보겠습니다. 테스트에 사용되는 주요 내용은 다음과 같습니다.

  • Ganashe가 생성한 계정 얻어오기
  • 컨트랙트 인스턴스 생성하기
    • deploy할 컨트랙트 객체 만들기
    • deploy하라는 트랜잭션을 전송하기
  • 네트워크에 deploy된 컨트랙트 object 내용 출력하기

상세 내용은 코드 주석을 참고하세요.


// import assert module to test with assertion
const assert= require( 'assert' );
// local ethereum test network
const ganache= require( 'ganache-cli' );
// web3 constructor function. note that it is Web3 not web3
const Web3= require( 'web3' );
// create web3 instance to connect local test network
const web3= new Web3( ganache.provider() );
// bytecode and api (interface) of compiled contract
const compiled_contract= require( '../ethereum/build/DreamStory.json' );

//// global variables
// ethereum accounts
let accounts;
// deployed contract
let dream_story;
// setup code before running a test
beforeEach( async () => {
    // get all the accounts
    accounts= await web3.eth.getAccounts();

    // create a contract instance with arguments and deploy it
    // parse the json interface, so the javascript object can be used for contract
    dream_story= await new web3.eth.Contract( JSON.parse( compiled_contract.interface ) )
      // tell web3 that we want to deploy a new conpy of the contract.
      // do not forget about the arguments that the constructor of the contract requires
      // calling deploy does not deploy the contract, it creates an object to be deployed
      .deploy({ data: compiled_contract.bytecode, arguments: [100] })
      // send transaction that creates the contract
      .send( { from: accounts[0], gas: '1000000' } )
});

// 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 );
    });
});


이렇게 작성한 후 다음과 같이 테스트를 실행합니다.

$ npm run test


그러면 아래와 같이 테스트가 패스하고, deploy된 컨트랙트 객체의 내용이 출력됩니다. 테스트를 실행한 후 약간의 시간이 걸리는 게 느껴지나요? 로컬 네트워크라고 해도 컨트랙트 객체를 deploy하라는 트랜잭션을 전송하는 데 시간이 소요됩니다. 메인 네트워크라면 더 많은 시간이 걸리겠죠.



지금까지 해본것을 정리하면 다음과 같습니다.

  • 스마트 컨트랙트 소스 파일을 자동으로 컴파일할 수 있는 스크립트
  • 로컬 테스트 네트워크에 접속하는 방법
  • 컨트랙트 객체를 생성하고 배포하는 테스트 스크립트

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



오늘의 실습: 작성한 스마트 컨트랙트에서 어떤 부분을 중점적으로 테스트 해야 할까요?