크립토 좀비 - 7 [테스트 코드]

Lumi·2021년 11월 30일
0

크립토 좀비

목록 보기
7/7
post-thumbnail

🔥 개요

이전 시간까지 해서 solidity에 대한 전반적인 작업은 끝났다.

이후 Truffle를 이용해서 해당 솔리디티가 정상적으로 작동을 하는지 확인해볼 시간이다.

전반적인 코드 구성도는 이렇다.

  • 빌드까지 완료한 모습이다.

이후 우리는 test폴더에 js파일을 만들고 해당 js파일에 테스트 코드를 작성하여 정상적으로 작동하는지를 확인해야 한다.

🔥 전체 완성 코드

const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const expect = require('chai').expect
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })
    context("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
            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});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner, bob);
        })
    })
    context("with the two-step transfer scenario", async () => {
        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);
        })
        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);
         })
    })
    it("zombies should be able to attack another zombie", async () => {
        let result;
        result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        const firstZombieId = result.logs[0].args.zombieId.toNumber();
        result = await contractInstance.createRandomZombie(zombieNames[1], {from: bob});
        const secondZombieId = result.logs[0].args.zombieId.toNumber();
        await contractInstance.attack(firstZombieId, secondZombieId, {from: alice});
        assert.equal(result.receipt.status, true);
    })
})

굉장히 길지만 생각보다 반복되는 코드가 많다.

  • 천천히 하나씩 뜯어보자!!

🔥 기본 셋팅

const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
const expect = require('chai').expect

contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
}

딱 이정도가 기본 셋팅이라고 할 수가 있다.

일단 contract를 선언을 한뒤 그 값을 이용하여 테스트 코드를 짜는 것이다.

alice, bob은 일종의 테스트를 위한 사용자를 말하며

zombieNames는 일종의 테스트를 위한 좀비이다.

contractInstance는 일종의 solidity와 통신하기 위한 인스턴스?? 라고 생각을 하면된다.

테스트 할떄마다 contractInstance를 생성하면 코드가 반복이 되기 떄문에

beforeEach를 통해서 테스트마다 contractInstance를 새로운 인스턴스로 초기화를 해주는 것이다.

만약 beforeEach를 사용하지 않으면 테스트하는 it을 사용할떄마다

let contractInstance = await CryptoZombies.new()를 입력해 주어야 한다.

🔥 좀비 생성 검증

it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
})

테스트는 it을 통해서 하게 된다.

첫번쨰 인자에는 테스트할떄 보이는 구문?? 이라고 할 수가 있다.
alice는 하나의 좀비를 가지고 있어야 한다.

  • 이런식으로 뜨는 구문을 말한다.
  • 즉 어떤 부분을 테스트할지 알려주는 콘솔이라고 생각하면 편할것 같다.

일단 기본적으로 async를 사용해야 한다.

  • 솔리디티를 실행시키는데 시간이 걸리기 떄문에

앞서 선언한 솔리디티 인스턴스 contractInstance 를 활용하여 솔리디티에서 선언하였던 함수를 실행 가능하다.

  • from을 통해서 누가 실행자인지를 지정하는 것이다.

이제 result를 통해서 제대로된 값이 들어왔는지를 확인 가능하다.

receipt.status는 해당 트랜잭션이 정상적으로 이루어 졌는지를 확인한다.

logs[0].args를 통해서 로그에 접근 가능하다.

  • name은 좀비값을 가지고 있다.

두 테스트가 모두 통과하면 문제가 없기 떄문에 에러가 발생하지 않는다.

assert.equal은 다른 방법으로도 작성 가능하다.

expect(result.receipt.status).to.equal(true);
  • 둘은 모두 같은 기능을 제공한다.

🔥 좀비 중복 검증

it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
})

이 부분은 따로 코드를 정리하고 import하였기 떄문에 이렇게 작성이 가능한 것이다.

원래 코드로 작성이 되면

it("should not allow two zombies", async () => {
  try {
    await contractInstance.createRandomZombie(zombieNames[1], {from: alice});
    assert(true);
  }
  catch (err) {
    return;
  }
assert(false, "The contract did not throw.");
})

이게 기존의 코드이다.

일단 try를 통해서 시도를 한다.

  • 아마 문제 없이 try구문이 작동할 것이다.

하지만 assert를 true로 함으로써 바로 오류를 발생 시키는 것이다.

assert가 true면 바로 오류가 발생을 한다.

🔥 좀비 전송 과정

context("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
            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});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner, bob);
        })
    })

context는 일종의 테스트를 담는 곳이다.

1. 모든 것이 맞아야 한다.
 - A는 1이다.
 - B는 2이다.
 
이런식의 구문에서 1. 모든것이 맞아야 한다. 같은 것은 context로 작성을 하게 된다.

코드를 보면 계속해서 인스턴스를 통해서 랜덤한 좀비를 생성하는 것을 알 수가 있다.

  • 왜냐하면 테스트를 계속 초기화 하여 진행을 해야 하기 떄문이다.
  • 또한 이러한 코딩방식이 가능한 이유는 앞서 말했던 beforeEach 덕분이다.

우리가 솔리디티를 통해서 좀비를 고유한 값으로 설정을 하였고 이는 NFT라고도 할 수가 있다.

또한 그 NFT를 주고받을수 있어야 하기 떄문에 이러한 코드가 존재하는 것이다.

일단 좀비를 생성하고, 생성한 좀비에 대한 Id값을 받아 놓는다.

그후 솔리디티 코드에 있는 transferFrom을 통해서 전송을 한다.

  • alice에서 bob으로

이후 한 변수에 좀비 Id의 주인이 누구인지를 보고 그 값을 비교해 본다.

transferFrom을 통해서 alice의 좀비가 bob에게 갔기 떄문에 좀비의 주인은 bob이 되어야 정상이다.

🔥 좀비 전송 과정 - 2

context("with the two-step transfer scenario", async () => {
        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);
        })
        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);
         })
    })

우리가 토큰을 주고 받는 과정은 2가지가 있다.

앞선 방법에서는 전송자가 직접 함수를 실행하여 송신자에게 토큰을 전달하게 되지만

이부분은 주로 거래소에서 사용하는 방법으로 함수 실행자가 저장되어 있는 토큰을 가져가는 방식이다.

일단 기본적인 과정의 진행은 같다.

이떄 approve 함수를 실행함으로써 bob에게 alice가 하나의 좀비를 가상의 공간에 맡겨 둔 것이다.

  • 쉽게 말해 alice가 맡겨둔 값만 bob이 가져갈 수가 있다.

그후 transferFrombob이 호출하여 alice가 맡겨둔 것을 가져 간다.

이 부분은 이 코드를 보기 보다는 ERC - 20의 토큰 교환 방법 소스코드를 확인하는 것이 더 쉬울 것이다.

🔥 좀비 공격 이벤트

it("zombies should be able to attack another zombie", async () => {
        let result;
        result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        const firstZombieId = result.logs[0].args.zombieId.toNumber();
        result = await contractInstance.createRandomZombie(zombieNames[1], {from: bob});
        const secondZombieId = result.logs[0].args.zombieId.toNumber();
        await contractInstance.attack(firstZombieId, secondZombieId, {from: alice});
        assert.equal(result.receipt.status, true);
})

이 부분도 단순히 좀비를 생성하고 해당 좀비가 공격이 가능한지 즉 솔리디티 코드가 정상적으로 작동을 하는지 확인하는 코드이다.

🔥 후기

점차 어려워지고 처음하는 코드 작성이 많아지고 있다...

최대한 내가 이해한 것을 적어보고자 하였고 누군가를 이해시키기에는 내가 부족하여 안될 것 같다 ㅠ

profile
[기술 블로그가 아닌 하루하루 기록용 블로그]

0개의 댓글