[#7 Crypto Zombies] Truffle로 스마트 컨트랙트 테스트하기

cat_dev·2021년 3월 5일
0

Solidity

목록 보기
7/9
post-thumbnail

트러플 이용

프로젝트 구조

트러플을 사용해 빌드한 프로젝트 구조는 다음과 같이 된다.

├── build
  ├── contracts
      ├── Migrations.json
      ├── CryptoZombies.json
      ├── erc721.json
      ├── ownable.json
      ├── safemath.json
      ├── zombieattack.json
      ├── zombiefactory.json
      ├── zombiefeeding.json
      ├── zombiehelper.json
      ├── zombieownership.json
├── contracts
  ├── Migrations.sol
  ├── CryptoZombies.sol
  ├── erc721.sol
  ├── ownable.sol
  ├── safemath.sol
  ├── zombieattack.sol
  ├── zombiefactory.sol
  ├── zombiefeeding.sol
  ├── zombiehelper.sol
  ├── zombieownership.sol
├── migrations
└── test
. package-lock.json
. truffle-config.js
. truffle.js
  • test 폴더에 주목!

트러플 특징

  • 언어는 javascriptsolidity를 지원한다.
  • 이 튜토리얼에서는 간편한 작성을 위해 javascript로 코드를 짤 예정

테스트 코드 주의할 점

  • 테스트 코드를 짤 때에는 한 컨트랙트 당 하나의 테스트를 짜야 유지 보수에 좋다!

빌드

빌드 생성물

  • 스마트 컨트랙트를 빌드할 때 솔리디티 컴파일러는 자동으로 JSON 형식의 파일을 생성한다.
  • 이 파일 속에는 컨트랙트의 binary representation이 들어있다.
  • 빌드하면서 나온 JSON 파일은 build/contracts 폴더에 저장된다.

빌드 생성물 가져오기

const myAwesomeContract = artifacts.require(“myAwesomeContract”);
  • 이 코드는 컨트랙트 abstraction을 리턴한다.
  • 말 그대로 abstraction 이라서 뒤에서 자바스크립트 객체를 생성해서 그거로 컨트랙트와 통신해야한다.

Contract 함수

테스트를 단순하게 만들기 위해 TruffleMocha안에 얇은 wrapper를 만든다.

그룹 테스트

  • contract()함수를 불러서 실행하고, 이건 mochadescribe함수에 테스팅 어카운트 리스트를 제공하고 정리 좀 해주고 extends 한다.
  • contract()함수는 두개의 인자를 받는다. 어떤 것을 테스트할지 보여주는 string과 테스트를 write하는 장소를 보여주는 callback()

it()으로 테스트 실행하기

  • it()이라는 함수를 불러서 테스트를 실행한다.
  • 이것도 맨 처음에 어떤 것을 테스트할지 나타내는 string과 테스트 장소를 나타내는callback()으로 이루어져있다.

최종 테스트 코드

contract("MyAwesomeContract", (accounts) => {
   it("should be able to receive Ethers", await () => {
   })
 })
  • 이건 contractcallback()에서 it()을 부르는 형태
  • 자바스크립트 비동기적이니까 await써서 it()함수 사용

로컬 테스트

Ganache

  • 이더리움에 배포하기 전에 로컬 환경에서 스마트 컨트랙트를 테스트 해봐야 한다. Ganache라는 툴은 로컬 이더리움 네트워크를 셋업해준다.
  • Ganache는 시작할 때마다 10개의 테스트 어카운트를 만들고 각각 100이더를 지급한다.
  • GanacheTruffle은 잘 연동되어 있어서 우리는 accounts라는 배열로 어카운트에 접근할 수 있다.

접근하기

  • account[0], account[1]같은 것은 테스트 시 읽기가 매우 불편하다. 그래서 contract() 함수 안에서 아래처럼 initialize 해서 쓴다.
let [alice, bob] = accounts;

테스트 순서

Set up

  • initial stateinputdefine한다.

js 인스턴스와 abstraction 만들어서 테스트하기

const contractInstance = await myAwesomeContract.new();
  • 위에서 만든 abstractionmyAwesomeContract을 이용하여 실제 컨트랙트 통신에 이용할 수 있는 자바스크립트 인스턴스를 만든다.
//좀비 array를 글로벌로 선언하고
const zombieNames = ["Zombie #1", "Zombie #2"];

//아래처럼 좀비를 이용해 컨트랙트의 메소드를 실행할 수 있다.
//createRandomZombie는 좀비 객체를 받는 함수였을 것..
contractInstance.createRandomZombie(zombieNames[0]);
  • 자바스크립트 객체를 만든 후에 메소드를 이용하는 형식!

act

  • 실제로 코드를 테스트 하는 공간!
  • 항상 하나의 로직만 테스트하도록 해야한다

Truffle 내장 기능 이용해서 address지정하기

const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
  • Setup 부분 코드의 문제점은 좀비를 보내는 주소를 지정하지 못한다는 것이었다.
  • Truffle에서는 솔리디티의 주소 체계를 가져와서 이용할 수 있다. 따라서 createRandomZombie 함수에 {from: alice}를 넣어 msg.senderaddressset할 수 있도록 한다.

artifacts.require 이용

  • 트러플에서 제공하는 artifacts.require을 사용하면 result.logs[0].args.name 같이 이용할 수 있다.

result에는 무엇이 저장될까?

  • result.tx: 트랜잭션 해시값
  • result.receipt: 트랜잭션 영수증을 포함하는 object
  • result.receipt.status: true일 경우 트랜잭션 성공, 아닐경우 false를 리턴

assert

  • 결과물을 체크하는 공간

내장된 라이브러리 사용

  • equal(), deepEqual() 같이 빌트인된 assertion 을 이용한다.
  • assert.equal() 이런 식으로 사용!

최종 코드

 it("should be able to create a new zombie", async () => {
   //앞에 만든 abstraction으로 instance를 생성해서 사용
        const contractInstance = await CryptoZombies.new();
   //await으로 result에 반드시 값이 들어오도록 설정
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
   //영수증 받은거 맞는지 확인
        assert.equal(result.receipt.status, true);
   //좀비 이름이 같은지 확인
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })

Hooks

test 환경 자동설정

  • 테스트를 할 때마다 새롭게 인스턴스를 생성해야 하는데, Truffle에서는 이걸 beforeEach() 함수로 자동으로 설정해준다.
beforeEach(async () => {
  // let's put here the code that creates a new contract instance
});
  • contract.new() 대신에 beforeEach() 한 번이면 뚝딱 해결

beforEach()로 매번 생성? 매번 삭제는 afterEach()

//컨트랙트 삭제하는 kill 함수 정의
function kill() public onlyOwner {
   selfdestruct(owner());
}

//매번 실행이 끝날 때마다 컨트랙트를 kill
afterEach(async () => {
   await contractInstance.kill();
});
  • 블록체인 테스트서버 과부하를 막기 위해 테스트가 끝난 후 kill해서 컨트랙트를 destruct한다!

error 잡기

try {
    //try to create the second zombie
    await contractInstance.createRandomZombie(zombieNames[1], {from: alice});
    assert(true);
  }
  catch (err) {
    return;
  }
assert(false, "The contract did not throw.");
  • try-catch 사용해서 에러가 났을 때 에러 잡음!
  • 여기서 에러 나는 이유는 앨리스가 이미 좀비를 가지고 있는데 또 다른 좀비를 create 했기 때문이다.
  • 보통 이 에러 잡는 구문은 따로 분리해 helpers/utils에 적고 import 해온다.

Account 간 통신 test

ERC721 토큰 통신 방식

  1. ownertransferFrom()을 불러 receiver에게 토큰 전달
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
  1. ownerapprove를 부르고 ownerreceivertransferForm()을 보내는 형식
//approve 부르기
function approve(address _approved, uint256 _tokenId) external payable;

//transferForm 부르기
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
  • approve를 부르면 컨트랙트가 트랜잭션 정보(address)를 저장한다.
  • transferForm을 부르면 자동으로 msg.senderownerreceiver의 주소와 일치하는지를 검사한다.
  • 일치한다면 토큰을 보낸다.

Truffle이 제공하는 context() 이용

1번 케이스 테스트 코드

context("with the single-step transfer scenario", async () => {
    it("should transfer a zombie", async () => {
      // TODO: Test the single-step transfer scenario.
      //랜덤 좀비 생성 코드
      const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
      //좀비 아이디가 로그에 찍힌 좀비 아이디와 같은지 확인
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
      //새로운 주인이 bob이 되었는지 확인
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner, bob);
    })
})

2번 케이스 테스트 코드

 context("with the two-step transfer scenario", async () => {
   //alice가 transferFrom을 부름
        it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: bob});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner,bob);
        })
   //bob이 transferForm을 부름
        it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner,bob);
         })
    })
  • 위 테스트코드는 항상 모든 테스트를 검사함..
  • 따라서 아래처럼 x를 붙여 트러플이 검사를 스킵할 수 있도록 조정
//context 앞에 x
xcontext("with the single-step transfer scenario", async () => {
    it("should transfer a zombie", async () => {
      // TODO: Test the single-step transfer scenario.
    })
})
//it 앞에 x
context("with the single-step transfer scenario", async () => {
    xit("should transfer a zombie", async () => {
      // TODO: Test the single-step transfer scenario.
    })
})

테스트 결과

Contract: CryptoZombies
    ✓ should be able to create a new zombie (199ms)
    ✓ should not allow two zombies (175ms)
    with the single-step transfer scenario
      - should transfer a zombie
    with the two-step transfer scenario
      - should approve and then transfer a zombie when the owner calls transferFrom
      - should approve and then transfer a zombie when the approved address calls transferFrom


  2 passing (827ms)
  3 pending

Time traveling

이전 좀비 게임 test 문제점 - coolDown

function _createZombie(string _name, uint _dna) internal {
  uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
  zombieToOwner[id] = msg.sender;
  ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
  emit NewZombie(id, _name, _dna);
}
  • coolDown 시간 설정 때문에 한 번 좀비를 생성하면 하루동안 생성이 안됨.. 그래서 테스트 코드를 여러번 돌려볼 수 없음
  • 그래서 time traveling을 적용!

Ganache의 시간 여행 함수

시간 조정 함수 두 가지

  1. evm_increaseTime: 다음 블럭의 시간을 increase
  2. evm_mine: 새로운 블록을 mine

함수가 작동하는 원리

  • 새로운 블록이 채굴될 때마다 채굴자는 타임스탬프를 블록에 넣는다.
  • evm_increaseTime을 작동시킨다. 블록체인은 이미 기록된 블록은 변할 수가 없으니 당장 시간에는 아무 변화가 없고, 대신 다음 채굴되는 블록의 시간을 변화시킨다.
  • evm_mine을 통해 블록을 채굴하면 시간이 앞으로 당겨져있는 것을 볼 수 있다.

time travel 코드

await web3.currentProvider.sendAsync({
  jsonrpc: "2.0",
  method: "evm_increaseTime",
  params: [86400],  // there are 86400 seconds in a day
  id: new Date().getTime()
}, () => { });

web3.currentProvider.send({
    jsonrpc: '2.0',
    method: 'evm_mine',
    params: [],
    id: new Date().getTime()
});
  • 이건 helper 쪽으로 빼서 작성해놓는다.

가져와서 이용하는 코드

await time.increase(time.duration.days(1))
  • time 임포트해와서 가져다가 사용!

트러플에서 돌려보면?

Contract: CryptoZombies
    ✓ should be able to create a new zombie (119ms)
    ✓ should not allow two zombies (112ms)
    ✓ should return the correct owner (109ms)
    ✓ zombies should be able to attack another zombie (475ms)
    with the single-step transfer scenario
      ✓ should transfer a zombie (235ms)
    with the two-step transfer scenario
      ✓ should approve and then transfer a zombie when the owner calls transferFrom (181ms)
      ✓ should approve and then transfer a zombie when the approved address calls transferFrom (152ms)

Chai를 이용한 assertion

위의 assertion은 읽기가 힘들어서 chai의 assertion 모듈을 많이 따다가 쓴다!

Chai의 세 가지 Assertion type

That said, let's take a look at the three kinds of assertion styles bundled into Chai:

  1. expect: 체인의 natural language assertions 을 다음과 같이 제공한다.
let lessonTitle = "Testing Smart Contracts with Truffle";
expect(lessonTitle).to.be.a("string");
  1. should: should 로 시작하고 인터페이스와 비슷한 assertion을 제공한다.
let lessonTitle = "Testing Smart Contracts with Truffle";
lessonTitle.should.be.a("string");
  1. assert: notation 을 제공하고 브라우저에서 작동할 수 있는 추가적인 테스트를 제공한다.
let lessonTitle = "Testing Smart Contracts with Truffle";
assert.typeOf(lessonTitle, "string");

expect().to.equal()

let zombieName = 'My Awesome Zombie';
expect(zombieName).to.equal('My Awesome Zombie');
  • assertion들을 위 코드처럼 chai 문법으로 바꾸어 사용한다!

Loom에서 테스트하기

  • 위 코드를 고칠 필요 없이 바로 사용할 수 있음

Loom network object

    loom_testnet: {
      provider: function() {
        const privateKey = 'YOUR_PRIVATE_KEY';
        const chainId = 'extdev-plasma-us1';
        const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket';
        const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws';
        return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
      },
      network_id: 'extdev'
    }

트러플과 Loom 통신

truffle.js

const HDWalletProvider = require("truffle-hdwallet-provider");
const LoomTruffleProvider = require('loom-truffle-provider');
const mnemonic = "YOUR MNEMONIC HERE";
module.exports = {
    // Object with configuration for each network
    networks: {
        //development
        development: {
            host: "127.0.0.1",
            port: 7545,
            network_id: "*",
            gas: 9500000
        },
        // Configuration for Ethereum Mainnet
        mainnet: {
            provider: function() {
                return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/<YOUR_INFURA_API_KEY>")
            },
            network_id: "1" // Match any network id
        },
        // Configuration for Rinkeby Metwork
        rinkeby: {
            provider: function() {
                // Setting the provider with the Infura Rinkeby address and Token
                return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/<YOUR_INFURA_API_KEY>")
            },
            network_id: 4
        },
        // Configuration for Loom Testnet
        loom_testnet: {
            provider: function() {
                const privateKey = 'YOUR_PRIVATE_KEY';
                const chainId = 'extdev-plasma-us1';
                const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket';
                const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws';
                // TODO: Replace the line below
                return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
            },
            network_id: '9545242630824'
        }
    },
    compilers: {
        solc: {
            version: "0.4.25"
        }
    }
};

LoomProvider 이용

  • 현재 디폴트로 사용하고 있는 HDWallet대신 LoomProvider를 이용하고, 프로바이더로 account 정보를 보내야 한다.
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)
  • 이 코드를 아래처럼 바꾸어야 돌아감!
const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;
profile
devlog

0개의 댓글