diff --git a/jp/11/00-overview.md b/jp/11/00-overview.md new file mode 100644 index 0000000000..4f0b3f1be6 --- /dev/null +++ b/jp/11/00-overview.md @@ -0,0 +1,28 @@ +--- +title: Truffleを使ってスマートコントラクトをテストする +header: Truffleを使ってスマートコントラクトをテストする +roadmap: roadmap.jpg +path: solidity_advanced +position: 1 +publishedOn: Cryptozombies +--- + +よく来たな!前回のレッスンではこれくらいお手の物であることをお前は証明した。 + +メインネットにゲームをデプロイするがいい。勝利を噛みしめるんだ! + +ちょっと待てよ…もしかするとお前は _すでに考えが及んでいるかもしれない_ が。コントラクトをメインネットにデプロイすると、永久に残るだろう。もし何かミスがあれば、それも永久に残ってしまうのだ。まるでアンデッドのゾンビだな。 + +どれほどスキルが高かろうと、ミスや _**バグ**_ はつきものだ。ゾンビが攻撃すると100%勝利を収めてしまうといったような大きなミスはありえないと思うかもしれないが、起きる時には起きるのだ。 + +攻撃側が100%勝つなんてゲームとして成り立たないし、面白くもなんともないだろう。こんなバグがあるとお前の作ったゲームは死んだも同然で、新鮮な脳みそでゾンビを墓からおびき出すことができなくなってしまう。 + +このような恐ろしいことが起きないよう、ゲームをあらゆる面からテストする必要がある。 + +このレッスンを終えると、できるようになることだ: + +- `Truffle` + `Ganache` を使ってスマートコントラクトをテストする +- `Chai` を使ってアサーションの表現力を上げる +- `Loom` 上でテストする😉 + +早速取り掛かるぞ! diff --git a/jp/11/01.md b/jp/11/01.md new file mode 100644 index 0000000000..e52a38c94e --- /dev/null +++ b/jp/11/01.md @@ -0,0 +1,66 @@ +--- +title: セットアップ +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +skipCheckAnswer: false +material: + terminal: + help: + You should probably run `touch test/CryptoZombies.js`😉 + commands: + "touch test/CryptoZombies.js": + hint: touch test/CryptoZombies.js + output: | +--- +このレッスンでは、 **Truffle** 、 **Mocha** および **Chai** に注目して、 **イーサリアム** のスマートコントラクトをテストするのに必要な原理を説明しよう。これらのレッスンを最大限理解するためには、 **Solidity** と **JavaScript** の中レベルの知識が必要だぞ。 + +もし **Solidity** が初めてか復習したい時は、最初のレッスンから始めるといい。 + +もし **JavaScript** に不安があるなら、他でチュートリアルをやってからこのレッスンに戻ってくるといい。 + + +## 我々のプロジェクトを覗いてみよう + +前回のレッスンを受けていれば、ゾンビゲームの準備がおおむねできているはずだ。ファイル構成は次のようになっているだろう: + +``` +├── 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` フォルダを見えるか?これからここにテストを置くぞ。 + +_Truffle_ では _JavaScript_ と _Solidity_ でのテストがサポートされている。しかし、このレッスンでは簡単な _JavaScript_ を扱っていこう。 + + +# さあテストだ + +練習がてらコントラクトごとにテストファイルを分け、ファイル名をそれぞれのコントラクトの名称にする。長い目で見たときに簡単にテストを対応づけられるぞ。特にプロジェクトが大きく育って変化していった時に有用だ。 + +1. 右のターミナルで、`touch test/CryptoZombies.js`コマンドを叩け。 diff --git a/jp/11/02.md b/jp/11/02.md new file mode 100644 index 0000000000..fc0d0cf337 --- /dev/null +++ b/jp/11/02.md @@ -0,0 +1,77 @@ +--- +title: セットアップ (続き) +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + contract("CryptoZombies", (accounts) => { + it("should be able to create a new zombie", () => { + + }) + }) + +--- +どんどん行くぞ。このチャプターでは、テストを書いて実行できるようセットアップを続けるぞ。 + +## ビルドアーティファクト + +スマートコントラクトをコンパイルするたびに、 _Solidity_ のコンパイラーはコントラクトをバイナリー形式に変換したJSONファイル( **ビルドアーティファクト** として参照される)を作成し、 `build/contracts` フォルダーに保存している。 + +続いてマイグレーションを実行すると、 _Truffle_ がこのファイルを更新して、ネットワークの情報を付加する。 + +新しいテストスイートの書き出しは、テスト対象のコントラクトのビルドアーティファクトをロードするところから始める。こうすることで、 **Truffle** はコントラクトが理解できるような形式で関数の呼び出し部分をフォーマットする方法を知ることができるのだ。 + +簡単な例を見せよう。 + +`MyAwesomeContract` というコントラクトがあるとする。そのビルドアーティファクトをロードするには、こんな風に書く: + +```javascript +const MyAwesomeContract = artifacts.require(“MyAwesomeContract”); +``` + +この関数は **_コントラクトの抽象化_** と呼ばれるものを返す。一言で言えば、 **イーサリアム** との複雑性なやり取りを隠蔽し、 _Solidity_ で書いたスマートコントラクトに対して _JavaScript_ の便利なインターフェースを提供している。次のチャプターで使うぞ。 + +### contract() 関数 + +裏側では、 **Truffle** はテストをシンプルにするために **Mocha** の周囲を薄くラップしている。我々のコースは **イーサリアム** の開発に注力していることから、 _Mocha_ について時間をかけて説明することは控える。 _Mocha_ を深掘りしたければ、このレッスンが終わった後にでもMochaのサイトをチェックするといい。今はここで説明していることを理解すればいい - 使い方: + +- `contract()`という名の関数を呼び出すことで行われる **グループテスト** 。 **Mocha** の `describe()` を拡張したもので、**テストに必要なアカウントのリスト** とクリーンアップを提供する。 + + `contract()` は2つの引数を持つ。一つ目は、`string`型のテスト対象だ。二つ目は、`callback` で、ここに実際のテストコードを書く。 + +- **実行**: `it()` という関数を呼び出して行う。これも2つの引数を持つ: テストが何をするかを説明する `string` と `callback` だ。 + +まとめると、テストはこのようになる: + + ```javascript + contract("MyAwesomeContract", (accounts) => { + it("should be able to receive Ethers", () => { + }) + }) + ``` + +> 注: よく考えられているテストは、コードが実際に何をするか説明している。テストスイートの説明文とテストケースは **一貫した内容** になるよう心掛けるのだ。ドキュメントを書く時と同じだな。 + +テストは全てこのパターンに従って書くように。なんてことないだろう?😁 + +# さあテストだ + +`CryptoZombies.js` の空のファイルを作っておいてやったから、これを埋めていくぞ。 + +1. 1行目は `CryptoZombies` という名の `const` を宣言し、`artifacts.require` 関数の戻り値を代入せよ。引数にはテスト対象のコントラクト名を渡すんだ。 + +2. 次に上からテストコードをコピペせよ。 + +3. `contract()` の呼び出しを修正し、最初のパラメータに我々のスマートコントラクトの名称を渡せ。 + + > 注: `accounts` は気にするな。次のチャプターで説明する。 + +4. `it()` 関数の最初の引数(上記の例では"should be able to receive Ethers"となっている)は、テストの名称であるべきだ。新しいゾンビを作るところから始めるので、最初の引数は"should be able to create a new zombie"となる。 + +準備はできた。次のチャプターにいくぞ。 diff --git a/jp/11/03.md b/jp/11/03.md new file mode 100644 index 0000000000..063f0adef7 --- /dev/null +++ b/jp/11/03.md @@ -0,0 +1,71 @@ +--- +title: 最初のテスト - 新しいゾンビを生み出す +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + contract("CryptoZombies", (accounts) => { + //1. initialize `alice` and `bob` + it("should be able to create a new zombie", () => { //2 & 3. Replace the first parameter and make the callback async + }) + }) + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + contract("CryptoZombies", (accounts) => { + let [alice, bob] = accounts; + it("should be able to create a new zombie", async () => { + }) + }) + + +--- + +**イーサリアム** にデプロイする前に、ローカルでスマートコントラクトをテストするべきだ。 + +それにはGanacheというツールが使えるぞ。 **イーサリアム** のネットワークをローカル環境に作ってくれる。 + +_Ganache_ を起動すると、10個のテスト用アカウントと、それぞれのアカウントに100イーサずつ準備してくれるので簡単にテストできるようになる。 _Ganache_ と _Truffle_ は統合されたから、我々はこれらのアカウントに `accounts` を通してアクセスできる。前回のチャプターで触れたやつだ。 + +しかし `accounts[0]` や `accounts[1]` と書くと、テストコードの可読性が悪くなるな? + +可読性を上げるため、2つの名称を使う - アリスとボブだ。さあ、`contract()` 関数内で定義するぞ: + +```javascript +let [alice, bob] = accounts; +``` +> 注: 貧相な文法については許してくれ。 _JavaScript_ では変数名は小文字を使うと規約で決まっているのだ。 + +なぜアリスとボブなのか?それには大いなる伝統があるんだ。アリスとボブまたは"A と B"は暗号や物理学、プログラミングなど様々な分野で使われてきた。ここでは簡単に説明したが、興味深い歴史があるぞ。このレッスンが終わった後読んでみるといい。 + +さあ最初のテストをするぞ。 + +## 新しいゾンビを生み出す + +アリスが我々の素晴らしいゲームをプレイしたいそうだ。それであれば彼女が最初にしたいことは **自分のゾンビを生み出す🧟** ことだろう。そのためにフロントエンド(または我々のケースでは _Truffle_ )は `createRandomZombie` 関数を呼ばなければならない。 + +> 注: 参考までにコントラクトの _Solidity_ のコードを載せる: + + ```sol + function createRandomZombie(string _name) public { + require(ownerZombieCount[msg.sender] == 0); + uint randDna = _generateRandomDna(_name); + randDna = randDna - randDna % 100; + _createZombie(_name, randDna); + } + ``` + +この関数をテストするところから始めよう。 + +# さあテストだ + +1. `contract()` 関数の1行目に、 `alice` と `bob` の2つの変数を定義するぞ。さっき見せた通りだ。 + +2. 次に、 `it()` 関数を適切に呼び出したい。2つ目の引数( `callback` 関数)はブロックチェーンと「会話」する。つまりこの関数は非同期だということだ。 `async` キーワードが必要だぞ。 `await` キーワードをつけて呼び出せば、テストは処理が終わって戻り値が帰ってくるまで待つようになる。 + +> Promise がどのように動作するかはこのレッスンの範囲外だ。このレッスンが終わったら、公式ドキュメントを読んで知識を深めるといいぞ。 diff --git a/jp/11/04.md b/jp/11/04.md new file mode 100644 index 0000000000..f7fe3e2747 --- /dev/null +++ b/jp/11/04.md @@ -0,0 +1,79 @@ +--- +title: 最初のテスト - 新しいゾンビを生み出す(続き) +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const zombieNames = ["Zombie 1", "Zombie 2"]; + contract("CryptoZombies", (accounts) => { + let [alice, bob] = accounts; + it("should be able to create a new zombie", async () => { + // start here + }) + }) + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const zombieNames = ["Zombie 1", "Zombie 2"]; + + contract("CryptoZombies", (accounts) => { + let [alice, bob] = accounts; + it("should be able to create a new zombie", async () => { + const contractInstance = await CryptoZombies.new(); + }) + }) + +--- + +いいぞ!これで最初のテストで使うシェルができた。テストの書き方を見せてやろう。 + +Usually, every test has the following phases: + +通常、テストは次のフェーズで構成される: + + 1. **_セットアップ_**: 状態やインプットを定義する場所だ。 + + 2. **_動作_**: コードをテストする場所だ。 _1つのことだけテストする_ ことに気をつけろ。 + + 3. **_アサート_:** 結果をチェックする場所だ。 + +テストコードで何をすべきかさらに細かく見ていこう。 + +## 1. セットアップ + +チャプター2で _コントラクトの抽象化_ を作ったな。しかしその名が示す通り、これは抽象化にすぎない。スマートコントラクトと実際に対話するためには、コントラクトの **インスタンス** として動作する _JavaScript_ のオブジェクトを作らなければならない。 `MyAwesomeContract` の例で、 _コントラクトの抽象化_ を用いてインスタンスを作る方法はこうだ: + +```javascript +const contractInstance = await MyAwesomeContract.new(); +``` + +よし。さて次は? + +`createRandomZombie` を呼び、ゾンビの名前を引数として渡すぞ。そう、次のステップではアリスのゾンビに名前をつけなければならない。“Alice’s Awesome Zombie” でどうだ。 + +しかし、テストの度にこれをするとコードが汚くなっていまう。より良いアプローチとして、グローバル配列に定義する方法がある: + +```javascript +const zombieNames = ["Zombie #1", "Zombie #2"]; +``` + +コントラクトのメソッドを呼ぶ時にこう使う: + +```javascript +contractInstance.createRandomZombie(zombieNames[0]); +``` + + > 注: ゾンビの名前を配列に格納する方法は重宝するぞ。例えば、1体や2体ではなく1000体のゾンビを作ろうとした時だ😉。 + +# さあテストだ + +お前のために `zombieNames` 配列を準備しておいたからな。 + +1. コントラクトのインスタンスを作るぞ。 `contractInstance` という名の `const` を定義し、 `CryptoZombies.new()` 関数の結果を代入するんだ。 + +2. `CryptoZombies.new()` はブロックチェーンと「対話」する。つまり非同期の関数だ。関数を呼び出すところに `await` キーワードをつけるぞ。 diff --git a/jp/11/05.md b/jp/11/05.md new file mode 100644 index 0000000000..b364cb3e75 --- /dev/null +++ b/jp/11/05.md @@ -0,0 +1,93 @@ +--- +title: 最初のテスト - 新しいゾンビを生み出す(続き) +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const zombieNames = ["Zombie 1", "Zombie 2"]; + contract("CryptoZombies", (accounts) => { + let [alice, bob] = accounts; + it("should be able to create a new zombie", async () => { + const contractInstance = await CryptoZombies.new(); + // start here + }) + }) + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const zombieNames = ["Zombie 1", "Zombie 2"]; + + contract("CryptoZombies", (accounts) => { + let [alice, bob] = accounts; + it("should be able to create a new zombie", async () => { + const contractInstance = await CryptoZombies.new(); + const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); + assert.equal(result.receipt.status, true); + assert.equal(result.logs[0].args.name,zombieNames[0]); + }) + }) +--- +さて準備は整った。次のフェーズに移ろう…🧟🦆‍🧟🦆🧟🦆‍🧟🦆🧟🦆‍🧟🦆 + +## 2. 動作 + +ついにここまでたどり着いた。アリスのために新しいゾンビを生み出す関数を呼ぶところだ - `createRandomZombie`。 + +だが少し問題がある - メソッドに誰から呼ばれたか「知らせる」方法はあるのだろうか?言い換えれば - アリスが(ボブではなく)この新しいゾンビの所有者であることをどう知らせればいいだろう?🧐 + +何を隠そう…この問題は _コントラクトの抽象化_ が解決してくれる。 _Truffle_ の機能の1つで、 _Solidity_ に元々あるインターフェースをラップし、関数を呼ぶ時に特定のアドレスを引数として渡すことができるのだ。 + +以下が `createRandomZombie` を呼び出す方法だ。こうすると `msg.sender` にアリスのアドレスがセットされる: + +```javascript +const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); +``` + +さあ、ここでちょっとしたクイズだ: `result` には何が格納されると思う? + +説明させてくれ。 + +#### ログとイベント + +`artifacts.require` でテスト対象のコントラクトが明確になったら、 _Truffle_ はスマートコントラクトが生成したログを自動で取得する。従って、アリスの新しいゾンビの `name` をこのように取得できる: `result.logs[0].args.name` 。同様に、 `id` や `_dna` も取得できるぞ。 + +このような情報の他にも、`result` はトランザクションのお役立ち情報を渡してくれている: + +- `result.tx`: トランザクションのハッシュ値 +- `result.receipt`: トランザクションのレシートのようなものだ。 `result.receipt.status` が `true` なら、トランザクションが成功したことを意味する。`false` なら失敗したということだ。 + +> 注: データを保存するための安価なオプションとしてもログは使えるぞ。欠点は、スマートコントラクト自体からはアクセスできないことだな。 + +## 3. アサート + +このチャプターでは標準で組み込まれているアサーションモジュールを使っていくぞ。このモジュールには `equal()` や `deepEqual()` といったアサーション関数がある。簡単に説明すると、これらの関数は状態をチェックし、結果が期待通りでなければエラーを `投げる` 。単純な値の比較なら、 `assert.equal()` を使うぞ。 + +# さあテストだ + +最初のテストを終わらせようじゃないか。 + +1. `result` という名の `const` を定義し、`contractInstance.createRandomZombie` の結果を格納しろ。引数にゾンビの名前とオーナーが必要になるぞ。 + +2. `result` を受け取ったら `assert.equal` で2つの値をチェックするぞ - `result.receipt.status` と `true` だ。 + +この条件がtrueなら、テストに合格したとみなすことができる。安全のため、もう一つチェックを追加するぞ。 + +3. 次の行で `result.logs[0].args.name` と `zombieNames[0]` をチェックしろ。さっきと同じで `assert.equal` を使うんだぞ。 + +さあ、 `truffle test`コマンドを叩いて、最初のテストに合格したか確認する時だ。このコマンドで、 _Truffle_ は _"test"_ ディレクトリーにあるファイルを実行する。 + +実は我々がすでにやっておいてやった。出力はこのようになる: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (323ms) + + + 1 passing (768ms) +``` + +これで最初のテストは終了だ - よく頑張った!まだまだ次が待ち受けているから、次のレッスンへ行くぞ… diff --git a/jp/11/06.md b/jp/11/06.md new file mode 100644 index 0000000000..f7ca959274 --- /dev/null +++ b/jp/11/06.md @@ -0,0 +1,137 @@ +--- +title: ゲームの楽しさを維持する +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const zombieNames = ["Zombie 1", "Zombie 2"]; + contract("CryptoZombies", (accounts) => { + let [alice, bob] = accounts; + + // start here + + it("should be able to create a new zombie", async () => { + const contractInstance = await CryptoZombies.new(); + const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice}); + assert.equal(result.receipt.status, true); + assert.equal(result.logs[0].args.name,zombieNames[0]); + }) + + //define the new it() function + }) + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + 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 () => { + }) + }) +--- + +お見事!これでユーザーが新しいゾンビを生み出せると確信が持てたな👌🏻。 + +しかし、もしユーザーがこの関数を呼ぶことで無数のゾンビ軍団を作成できたら、ゲームとして面白くない。Chapter 4 of Lesson 2で `createZombieFunction()` に `require` 命令文を追加して、どのユーザーもゾンビを1体しか作れなくしたんだったな: + +```sol +require(ownerZombieCount[msg.sender] == 0) +``` + +この機能を動作確認しよう。 + +## フック + +ほんの数分🤞で複数のテストを実行できるが、これらのテストが上手くいくには毎回白紙の状態からテストを始める必要がある。従って、テストの度にスマートコントラクトのインスタンスを作成する: + +```javascript +const contractInstance = await CryptoZombies.new(); +``` + +1回書いておけば、_Truffle_ がテストの度に自動でインスタンスを作ってくれればいいのに。 + +なんと... _Mocha_ (と _Truffle_ )の機能で、 _フック_ と呼ばれるコードスニペットがある。これはテストの前後で実行される。テスト実行前になんらかの処理を実行するには、 `beforeEach()` という関数の内部に処理を書いておくといい。 + +何回も `contract.new()` を書く代わりに、こうするんだ: + +```javascript +beforeEach(async () => { + // ここでコントラクトのインスタンスを生成する +}); +``` + +こうしておくと、`Truffle` が後はやってくれる。なんて良い子なんだ? + +# さあテストだ + +1. `alice` と `bob` を定義している行の下に、`contractInstance` という変数を定義しろ。ここでは値を代入しない。 + + > 注: `contractInstance` のスコープはブロック内に留めたい。なので `var` ではなく `let` を使うんだぞ。 + +2. 上のスニペットから `beforeEach()` 関数をコピペするんだ。 + +3. さあ、関数の中身を埋めよう。コントラクトのインスタンスを作成している行を `beforeEach()` 関数の中に **移動** せよ。`contractInstance` は別の場所に定義したから、`const` 修飾子は削除する。 + +5. 空の `it` 関数を作って、テスト名( `it` 関数の最初の引数)を "should not allow two zombies" とせよ。 + +次のチャプターで引き続き編集しよう! + +--- + +### 🧟‍♂️ここには... あらゆる種類のゾンビが揃っている!!!🧟‍♂️ + +もしお前が本当に、本当に **_習得する_** ことを望んでいるなら、続きを読むんだ。そうでないなら... NEXTをクリックして次のチャプターへ行くといい。 + +まだ残っていたか?😁 + +素晴らしい!お前は期待の星だ。 + +`contract.new` の動作の話に戻ろう。基本的には、この関数を呼ぶ度に _Truffle_ がコントラクトを新たにデプロイしている。 + +テストを毎回白紙の状態で始めてくれるのだから便利だと言える。 + +一方で、皆が無限にコントラクトを作成すると、ブロックチェーンが肥大化する。ブロックチェーンは自由に使ってもらっていいが、古いテストをいつまでも残しておいてもらっては困る! + +ブロックチェーンの肥大化を防ぎたいな? + +幸い、簡単に解決できる... 必要がなくなった時点で、コントラクトを `selfdestruct` (自動で消滅)させるのだ。 + +それにはこのようにする: + +- **最初に** 、`CryptoZombies` スマートコントラクトに、このような関数を追加する: + + ```sol + function kill() public onlyOwner { + selfdestruct(owner()); + } + ``` + > 注: `selfdestruct()` についてもっと知りたければ、ここに _Solidity_ のドキュメントがある。`selfdestruct` はブロックチェーンから特定のアドレスを消す _唯一の_ 手段であるということを心に留めておけ。とても重要な機能だ! + +- **次に** 、先ほど話した `beforeEach()` と似たもので、 `afterEach()` という関数を作る: + + ```javascript + afterEach(async () => { + await contractInstance.kill(); + }); + ``` + +- **最後に** 、テスト実行後に _Truffle_ がこの関数を呼んでくれる。 + +さあどうだ!スマートコントラクトが自動で消滅したぞ! + +このレッスンで伝えることは山ほどあるから、この機能を完全に説明しようとすると最低でも2つはチャプターを増やさなければならない。お前が追加してくれると期待している。💪🏻 diff --git a/jp/11/07.md b/jp/11/07.md new file mode 100644 index 0000000000..52924cbd86 --- /dev/null +++ b/jp/11/07.md @@ -0,0 +1,124 @@ +--- +title: ゲームの楽しさを維持する(続き) +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const utils = require("./helpers/utils"); + 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 () => { + // start here + }) + }) + "test/helpers/utils.js": | + async function shouldThrow(promise) { + try { + await promise; + assert(true); + } + catch (err) { + return; + } + assert(false, "The contract did not throw."); + + } + + module.exports = { + shouldThrow, + }; + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const utils = require("./helpers/utils"); + + 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})); + }) + }) +--- + +このチャプターでは、2つ目のテストの中身を埋めていく。やるべきことはこれだ: +- まず、アリスは `createRandomZombie` を呼び、1体目のゾンビに `zombieNames[0]` と名付ける。 +- 次に、アリスは2体目のゾンビを生み出そうとする。先ほどと1か所違う点は、ゾンビの名前を `zombieNames[1]` にすることだ。 +- この時点で、コントラクトがエラーを `投げて` くれることを我々は期待している。 +- スマートコントラクトがエラーを吐くとテストを合格とすることから、ロジックは少し変わってくる。 `createRandomZombie` 関数を2回目に呼び出すときは、 `try/catch` ブロックの中で行うのだ: + +```javascript +try { + // 2体目のゾンビを作成 + await contractInstance.createRandomZombie(zombieNames[1], {from: alice}); + assert(true); + } + catch (err) { + return; + } +assert(false, "The contract did not throw."); +``` +さあ、これで期待通り動くだろうか? + +うーむ... あと少しというところだな。 + +テストコードを整理するため、上記のコードを `helpers/utils.js` に移動し、 “CryptoZombies.js” にインポートする: + +```javascript +const utils = require("./helpers/utils"); +``` + +そして、コードの呼び出しはこのようになる: + +```javascript +await utils.shouldThrow(myAwesomeContractInstance.myAwesomeFunction()); +``` + +# さあテストだ + +前のチャプターで、2つ目のテスト用に空のシェルを作成したな。それを埋めていくぞ。 + +1. まず、アリスの1体目のゾンビを生み出そう。 `zombieNames[0]` と名づけ、オーナーを渡すことを忘れるなよ。 + +2. アリスの1体目のゾンビを生み出したら、`shouldThrow` に `createRandomZombie` を引数として渡すんだ。書き方を思い出せなら先ほどの例を見に行け。だがまずは見ずに書いてみることだ。 + +素晴らしい、2つ目のテストが完成したぞ! + +`truffle test` を実行してやったぞ。ここに出力がある: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (129ms) + ✓ should not allow two zombies (148ms) + + + 2 passing (1s) +``` + +テストに合格した。万歳! diff --git a/jp/11/08.md b/jp/11/08.md new file mode 100644 index 0000000000..af45efa50a --- /dev/null +++ b/jp/11/08.md @@ -0,0 +1,201 @@ +--- +title: ゾンビの譲渡 +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const utils = require("./helpers/utils"); + 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})); + }) + + // start here + }) + "test/helpers/utils.js": | + async function shouldThrow(promise) { + try { + await promise; + assert(true); + } + catch (err) { + return; + } + assert(false, "The contract did not throw."); + + } + + module.exports = { + shouldThrow, + }; + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const utils = require("./helpers/utils"); + + 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})); + }) + xcontext("with the single-step transfer scenario", async () => { + it("should transfer a zombie", async () => { + // TODO: Test the single-step transfer scenario. + }) + }) + xcontext("with the two-step transfer scenario", async () => { + it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => { + // TODO: Test the two-step scenario. The approved address calls transferFrom + }) + it("should approve and then transfer a zombie when the owner calls transferFrom", async () => { + // TODO: Test the two-step scenario. The owner calls transferFrom + }) + }) + }) +--- +質問- アリスがゾンビをボブにあげたいそうだ。これをテストしようじゃないか? + +もちろん! + +前回のレッスンを受けていれば、我々のゾンビが _ERC721_ に準拠していると知っているはずだ。そして、 _ERC721_ の仕様では、トークンを移動する方法が2つある: + +**(1)** +```sol +function transferFrom(address _from, address _to, uint256 _tokenId) external payable; +``` + +1つ目の方法では、アリス(ゾンビのオーナー)が `transferFrom` を呼び、`_from` にアリスの `address` 、`_to` にボブの `address` 、`zombieId` には譲渡対象のゾンビを渡す。 + +**(2)** +```sol +function approve(address _approved, uint256 _tokenId) external payable; +``` + +続いて + +```sol +function transferFrom(address _from, address _to, uint256 _tokenId) external payable; +``` + +2つ目の方法では、まずアリスが `approve` を呼び、ボブのアドレスと `zombieId` を渡す。コントラクトはボブがゾンビを受け取ることを承認した状態となる。次に、アリスまたはボブが `transferFrom` を呼ぶと、コントラクトは `msg.sender` がアリスかボブのアドレスであることをチェックする。チェックできたら、ゾンビはボブの物になる。 + +ゾンビを譲渡するこの2つの方法を「シナリオ」と呼ぶ。それぞれのシナリオをテストするため、2つのテストグループを作成し、分かりやすい説明文をつけたい。 + +なぜグループにするかって?少しテストするだけなのに... + +現時点ではテストコードはすっきりしているが、常にそうだとは限らない。2つ目のシナリオ(`approve` に続いて `transferFrom` を実行する)では少なくとも2つテストが必要だからな: + +- まず、アリスが単独でゾンビを譲渡できるかチェックする。 + +- 次に、ボブが `transferFrom` を呼び出せるかもチェックしなければならない。 + +さらに言えば、将来的に機能を追加してテストを増やす必要が出てくるかもしれない。最初からスケーラブルな構造にしておくことが最適だと我々は信じている😉。外部の人間にとってもお前のコードを理解しやすくなるし、お前自身他のことに取り組んで時間が経った後で見返したときにも役立つだろう。 + +> 注: 他のプログラマと一緒に仕事するようになった時、彼らは最初に書いたコードの規則に準拠して書いてくれるだろう。効率的に連携できるスキルは、大きなプロジェクトを成功させる上で不可欠なものなる。できるだけ早い段階で良い癖を身に着けておけば、お前のプログラマー人生を成功に導いてくれるぞ。 + +## context関数 + +To group tests, _Truffle_ provides a function called `context`. Let me quickly show you how to use it in order to better structure our code: + +テストをグループ化するため、 _Truffle_ は `context` という関数を提供している。どのようにコードを整理するか、さっと見せてやろう。 + +```javascript +context("with the single-step transfer scenario", async () => { + it("should transfer a zombie", async () => { + // TODO: 1ステップで送るシナリオをテスト + }) +}) + +context("with the two-step transfer scenario", async () => { + it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => { + // TODO: 2ステップのシナリオをテスト。受け取り側がtransferFromを呼ぶ。 + }) + it("should approve and then transfer a zombie when the owner calls transferFrom", async () => { + // TODO: 2ステップのシナリオをテスト。オーナーがtransferFromを呼ぶ。 + }) +}) +``` + +これを `CryptoZombies.js` ファイルに追加し、`truffle test`コマンドを叩くと、出力はこれに似た感じになる: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (100ms) + ✓ should not allow two zombies (251ms) + 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 + + + 5 passing (2s) +``` + +どうだ? + +うーむ... + +もう一度見てみよう - 上記の出力だと1つ問題がある。全てのテストが合格したように見えるが、まだテストコードを書いてさえいないのだからfalseになるべきだろう!! + +幸い、簡単に解決できる- `context()` 関数の前に `x` をつける: `xcontext()`。こうすることで `Truffle` はテストをスキップしてくれるのだ。 + +> 注: `x` は `it()` 関数の前にも同様に置くことができる。テストコードを書き終わったら x を外すことを忘れるんじゃないぞ! + +`truffle test`コマンドを叩こう。出力はこんな感じになるはずだ: + +``` +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 +``` + +「-」はテストに「x」マーカーがついていてスキップされたことを意味している。 + +めっちゃ簡単だな?テストコードを書きつつ実行できるようになった。近いうちに書かなければならないテストコードにはさっきのマークをつけるんだぞ。 + +# さあテストだ + +1. コードを上の例からコピぺしろ。 + +2. ここでは、 `context` 関数を _スキップ_ するようにするぞ。 + + +テストコードは空のシェルに過ぎないから、ここに多くのロジックを実装しなければならない。今後のチャプターで一つ一つやっていこう。 diff --git a/jp/11/09.md b/jp/11/09.md new file mode 100644 index 0000000000..cd05ace156 --- /dev/null +++ b/jp/11/09.md @@ -0,0 +1,157 @@ +--- +title: ERC721 トークンの移転- 1ステップのシナリオ +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const utils = require("./helpers/utils"); + 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})); + }) + xcontext("with the single-step transfer scenario", async () => { + it("should transfer a zombie", async () => { + // start here. + }) + }) + xcontext("with the two-step transfer scenario", async () => { + it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => { + // TODO: Test the two-step scenario. The approved address calls transferFrom + }) + it("should approve and then transfer a zombie when the owner calls transferFrom", async () => { + // TODO: Test the two-step scenario. The owner calls transferFrom + }) + }) + }) + "test/helpers/utils.js": | + async function shouldThrow(promise) { + try { + await promise; + assert(true); + } + catch (err) { + return; + } + assert(false, "The contract did not throw."); + + } + + module.exports = { + shouldThrow, + }; + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const utils = require("./helpers/utils"); + + 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); + }) + }) + xcontext("with the two-step transfer scenario", async () => { + it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => { + // TODO: Test the two-step scenario. The approved address calls transferFrom + }) + it("should approve and then transfer a zombie when the owner calls transferFrom", async () => { + // TODO: Test the two-step scenario. The owner calls transferFrom + }) + }) + }) +--- + +これまではウォーミングアップにすぎない... + +しかしついにお前の知識を披露する時がきた! + +次のチャプターでは、今まで学んできたことを総動員して美しいテストコードを書いていく。 + +手始めに、_アリス_ が _ボブ_ にERC721トークンを1ステップで送るシナリオをテストするぞ。 + +これがテストコードに書くべきことだ: + +- アリスに新しいゾンビを作ってやる(ゾンビはERC721トークンであることを思い出せ)。 + +- アリスがボブにERC721トークンを送る。 + +- この時点で、ボブがERC721トークンを所有している。そうであれば、 `ownerOf` はボブのアドレスを返すだろう。 + +- `assert` を使ってボブが `新たな所有者` になったことをチェックするぞ。 + + +# さあテストだ + +1. 1行目で `createRandomZombie` を呼ぶぞ。ゾンビの名前として `zombieNames[0]` を渡し、アリスをオーナーとするのだ。 + +2. 2行目で `zombieId` という名の `const` を宣言し、作ったゾンビのidを代入しろ。Chapter 5でスマートコントラクトからログとイベントを取得する方法を学習したぞ。必要なら復習するんだ。`toNumber()` で `zombieId` を数値に変換する必要があるぞ。 + +3. 続いて `transferFrom` を呼び、 `alice` と `bob` を最初の引数として渡す。アリスがこの関数を呼び、結果を待って(`await`)から次のステップに移る必要があることに注意しろ。 + +4. `newOwner` という名の `const` を定義せよ。`zombieId` で `ownerOf` を呼び出した結果を格納する。 + +5. 最後に、ボブがERC721トークンを所有しているかチェックしよう。これをコードにすると、 `assert.equal` に `newOwner` と `bob` を引数として渡すんだ。 + + > 注: `assert.equal(newOwner, bob)` と `assert.equal(bob, newOwner)` は同じものだ。しかし我々のコマンドラインのインタプリタはそれほど優秀でないから、1つ目の書き方でないと正解と判定できないぞ。 + +6. さっき「最後」と言ったっけ?うむ... それは嘘だ。最後にすべきことは、最初のシナリオのスキップしている `x` を外すことだ。 + +ふう!たくさんコードを書いたな。ちゃんとできたか?分からなければ「答えを表示」で確認しよう。 + + +さあ、`truffle test`コマンドを叩いて、テストが合格するか見てみるぞ: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (146ms) + ✓ should not allow two zombies (235ms) + with the single-step transfer scenario + ✓ should transfer a zombie (382ms) + 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 + + +3 passing (1s) +2 pending +``` + +できたじゃないか!見事テストに合格したな👏🏻。 + +次のチャプターでは、2ステップのシナリオに取りかかろう。`approve` の後に `transferFrom` を呼ぶやつだ。 diff --git a/jp/11/10.md b/jp/11/10.md new file mode 100644 index 0000000000..22febc5be1 --- /dev/null +++ b/jp/11/10.md @@ -0,0 +1,164 @@ +--- +title: ERC721 トークンの移転- 2ステップのシナリオ +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const utils = require("./helpers/utils"); + 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); + }) + }) + xcontext("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(); + // start here + await contractInstance.transferFrom(alice, bob, zombieId, {from: alice}); + const newOwner = await contractInstance.ownerOf(zombieId); + assert.equal(newOwner,bob); + }) + it("should approve and then transfer a zombie when the owner calls transferFrom", async () => { + // TODO: Test the two-step scenario. The owner calls transferFrom + }) + }) + }) + "test/helpers/utils.js": | + async function shouldThrow(promise) { + try { + await promise; + assert(true); + } + catch (err) { + return; + } + assert(false, "The contract did not throw."); + + } + + module.exports = { + shouldThrow, + }; + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const utils = require("./helpers/utils"); + + 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); + }) + xit("should approve and then transfer a zombie when the owner calls transferFrom", async () => { + // TODO: Test the two-step scenario. The owner calls transferFrom + }) + }) + }) +--- + +さて、ここまで来れば `approve` の後に `transferFrom` を呼ぶやり方でERC721トークンを移転するなんて簡単だろうが、手伝ってやろう。 + +一言で言えば、我々は2種類のシナリオをテストしなければならない: + +- アリスはボブがERC721トークンを受け取ることを承認する。それから、ボブ(**承認されたアドレス**)が `transferFrom` を呼ぶ。 + +- アリスはボブがERC721トークンを受け取ることを承認する。次に、アリスがERC721トークンを移転する。 + +2つのシナリオの違いは、アリスかボブの _**どちらが**_ 移転を実行するかだ。 + +理解できたかな? + +最初のシナリオを見ていこう。 + +## BobがtransferFromを呼び出す + +このシナリオのステップは次の通りだ: + +- アリスがERC721トークンを作成し、`approve` を呼ぶ。 +- 次に、ボブが `transferFrom` を実行し、ERC721トークンの所有者となる。 +- 最後に、 `assert.equal` で `newOwner` と `bob` が一致することを確認する。 + +# さあテストだ + +1. 最初の2行は前回のテストと同じだ。コピペしておいてやったぞ。 + +2. 次に、ボブがERC721トークンを受け取ることを承認するため、`approve()` を呼ぶ。この関数は `bob` と `zombieId` を引数として受け取る。アリスがこのメソッドを呼ぶんだぞ(移転するERC721トークンの持ち主は彼女だからだ)。 + +3. 最後の3行は前のテストと **大体同じ** だ。ここにもコピペしておいてやったぞ。 `transferFrom()` 関数をボブが呼ぶように変更するんだ。 + +4. 最後に、このシナリオの「スキップを外し」、最後のテストケースはまだ書いていないから「スキップ」するようにする。 + +`truffle test`コマンドを叩いて、テストが合格するか見るぞ: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (218ms) + ✓ should not allow two zombies (175ms) + with the single-step transfer scenario + ✓ should transfer a zombie (334ms) + with the two-step transfer scenario + ✓ should approve and then transfer a zombie when the owner calls transferFrom (360ms) + - should approve and then transfer a zombie when the approved address calls transferFrom + + + 4 passing (2s) + 1 pending +``` + +素晴らしい!さあ、次のテストに移ろう。 diff --git a/jp/11/11.md b/jp/11/11.md new file mode 100644 index 0000000000..e4c094f4d2 --- /dev/null +++ b/jp/11/11.md @@ -0,0 +1,142 @@ +--- +title: ERC721 トークンの移転- 2ステップのシナリオ(続き) +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const utils = require("./helpers/utils"); + 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); + }) + xit("should approve and then transfer a zombie when the owner calls transferFrom", async () => { + // TODO: start + }) + }) + }) + "test/helpers/utils.js": | + async function shouldThrow(promise) { + try { + await promise; + assert(true); + } + catch (err) { + return; + } + assert(false, "The contract did not throw."); + + } + + module.exports = { + shouldThrow, + }; + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const utils = require("./helpers/utils"); + + 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); + }) + }) + }) +--- +移転のテストはもう少しで完了だ!さあ、アリスが `transferFrom` を呼ぶシナリオをテストしようじゃないか。 + +良い知らせだ- このテストは簡単だ。前のチャプターからテストコードをコピぺし、`transferFrom` を **アリス** (ボブではない)が呼ぶようにするだけだ。 + +# さあテストだ + +1. 前回のテストコードをコピペし、`transferFrom` をアリスが呼ぶようにするんだ。 +2. 「スキップ」を外す。これで完了だ。 + +`truffle test`コマンドを叩けば、出力はこんな感じになる: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (201ms) + ✓ should not allow two zombies (486ms) + ✓ should return the correct owner (382ms) + with the single-step transfer scenario + ✓ should transfer a zombie (337ms) + with the two-step transfer scenario + ✓ should approve and then transfer a zombie when the approved address calls transferFrom (266ms) + 5 passing (3s) +``` + +移転で他にテストすべきことは思いつかない。だからこれで完了だ。 diff --git a/jp/11/12.md b/jp/11/12.md new file mode 100644 index 0000000000..7f59650094 --- /dev/null +++ b/jp/11/12.md @@ -0,0 +1,326 @@ +--- +title: ゾンビの攻撃 +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const utils = require("./helpers/utils"); + const time = require("./helpers/time"); + 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(); + //TODO: increase the time + await contractInstance.attack(firstZombieId, secondZombieId, {from: alice}); + assert.equal(result.receipt.status, true); + }) + }) + "test/helpers/utils.js": | + async function shouldThrow(promise) { + try { + await promise; + assert(true); + } + catch (err) { + return; + } + assert(false, "The contract did not throw."); + + } + + module.exports = { + shouldThrow, + }; + + "test/helpers/time.js": | + async function increase(duration) { + + //first, let's increase time + await web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_increaseTime", + params: [duration], // there are 86400 seconds in a day + id: new Date().getTime() + }, () => {}); + + //next, let's mine a new block + web3.currentProvider.send({ + jsonrpc: '2.0', + method: 'evm_mine', + params: [], + id: new Date().getTime() + }) + + } + + const duration = { + + seconds: function (val) { + return val; + }, + minutes: function (val) { + return val * this.seconds(60); + }, + hours: function (val) { + return val * this.minutes(60); + }, + days: function (val) { + return val * this.hours(24); + }, + } + + module.exports = { + increase, + duration, + }; + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const utils = require("./helpers/utils"); + + const time = require("./helpers/time"); + + 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 time.increase(time.duration.days(1)); + await contractInstance.attack(firstZombieId, secondZombieId, {from: alice}); + assert.equal(result.receipt.status, true); + }) + }) +--- +わお!これまでのチャプターは情報量が多かったが、そのおかげで色んなことを学んだ。 + +これでシナリオは全て終わったかって?いや、まだだ。最後にラスボスを残しておいた。 + +ゾンビゲームの **見どころ** はゾンビ同士が戦うところだろう? + +これのテストは簡単で、次のステップを踏む: + +- **まず**、 ゾンビを2体作成する - 1体はアリスので、もう1体はボブのだ。 +- **次に**、アリスが彼女のゾンビでボブの `zombieId` を `攻撃` する。 +- **最後に**、`result.receipt.status` が `true` であればテストに合格したものとする。 + +この説明をしている間に `it()` 関数を用意してちゃちゃっとロジックを実装し、`truffle test` を実行しておいた。 + +出力はこんな感じだろう: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (102ms) + ✓ should not allow two zombies (321ms) + ✓ should return the correct owner (333ms) + 1) zombies should be able to attack another zombie + with the single-step transfer scenario + ✓ should transfer a zombie (307ms) + with the two-step transfer scenario + ✓ should approve and then transfer a zombie when the approved address calls transferFrom (357ms) + + + 5 passing (7s) + 1 failing + + 1) Contract: CryptoZombies + zombies should be able to attack another zombie: + Error: Returned error: VM Exception while processing transaction: revert + +``` + +あれま。テストが失敗してしまった☹️。 + +なぜだ? + +確かめてみよう。まず、`createRandomZombie()` のコードを確認してみるぞ: + +```sol +function createRandomZombie(string _name) public { + require(ownerZombieCount[msg.sender] == 0); + uint randDna = _generateRandomDna(_name); + randDna = randDna - randDna % 100; + _createZombie(_name, randDna); +} +``` + +ここまではよさそうだ。続いて `_createZombie()` の中身を見てみよう: + +```sol +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); +} +``` + +おお、問題が見つかったか? + +テストが失敗したのは **クールダウン** 期間を設定したからだ。攻撃(またはごはん)の後 **1日** 待たないと再び攻撃できないのだ。 + +こうしなければ、ゾンビは1日の間に何度でも攻撃できるようになり、ゲームがめちゃくちゃ簡単になってしまう。 + +さて、我々はこれから... 1日待つのか? + +## タイムトラベル + +幸いにも、我々はそれほど待たなくてよい。実は全く待つ必要はないのだ。_Ganache_ が時間を進める機能を2つ提供してくれいてるからだ: + + - `evm_increaseTime`: 次のブロックのために時計を進める。 + - `evm_mine`: 新しいブロックをマイニングする。 + +タイムトラベルをするのにターディス(ドクター・フー)やデロリアン(バック・トゥー・ザ・フューチャー)を用意する必要はないぞ。 + +これらの関数の動きについて説明させてくれ: + +- 新しいブロックをマイニングすると、マイナーはタイプスタンプを付与する。ゾンビを作成するトランザクションはブロック5にマイニングされたとしよう。 + +- 次に、`evm_increaseTime` を呼ぶのだが、ブロックチェーンは不変だから、すでに存在するブロックを変更する手段はない。コントラクトがブロックの時間を確認したときに、時間が増えていることはない。 + +- `evm_mine` を実行し、ブロック6がマイニングされる(時計を進めたタイムスタンプが付与される)。ゾンビ同士を戦わせる時にスマートコントラクトがブロック6の時間を「確認」すると、1日経っているというからくりだ。 + +以上を踏まえ、テストコードを修正してタイムトラベルできるようにする: + +```javascript +await web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_increaseTime", + params: [86400], // 1日は 86400 秒 + id: new Date().getTime() +}, () => { }); + +web3.currentProvider.send({ + jsonrpc: '2.0', + method: 'evm_mine', + params: [], + id: new Date().getTime() +}); +``` + +ああ、良いコードだろう。だが、このロジックを `CryptoZombies.js` ファイルには追加したくない。 + +そこで `helpers/time.js` という名のファイルを追加し、そこにコードを移動させたぞ。時計を進めるには、これを呼ぶだけだ: `time.increaseTime(86400);` + +これでもまだ充分とは言えない。別に1日は何秒であるかを覚えていて欲しいわけではない。 + +だから追加で `days` という名の _ヘルパー関数_ を用意してやった。この関数は何日時計を進めたいかを引数として受け取る。このように関数を呼ぶんだ: `await time.increase(time.duration.days(1))` + +> 注: 当然、タイムトラベルはメインネットやマイナーによって保護されているテストチェーンでは使えない。誰かが現実世界で時間を変えることができたら、めちゃくちゃになってしまう!スマートコントラクトのテストするには、開発者にとってタイムトラベルというレパートリーは欠かせない。 + +# さあテストだ + +失敗したテストを書いておいてやったぞ。 + +1. コメントを残しておいたから、そこのテストケースを修正するんだ。さっきの `await time.increase` を使うぞ。 + +準備は整った。`truffle test`コマンドを叩け: + +``` +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) +``` + +できたじゃないか!これがこのチャプターの最後のステップだ。 diff --git a/jp/11/13.md b/jp/11/13.md new file mode 100644 index 0000000000..9906dfdd8a --- /dev/null +++ b/jp/11/13.md @@ -0,0 +1,263 @@ +--- +title: Chai の表現力豊かなアサーション +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "test/CryptoZombies.js": | + const CryptoZombies = artifacts.require("CryptoZombies"); + const utils = require("./helpers/utils"); + const time = require("./helpers/time"); + //TODO: import expect into our project + 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}); + //TODO: replace with expect + 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); + //TODO: replace with expect + 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); + //TODO: replace with expect + 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); + //TODO: replace with expect + 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 time.increase(time.duration.days(1)); + await contractInstance.attack(firstZombieId, secondZombieId, {from: alice}); + //TODO: replace with expect + assert.equal(result.receipt.status, true); + }) + }) + + "test/helpers/utils.js": | + async function shouldThrow(promise) { + try { + await promise; + assert(true); + } + catch (err) { + return; + } + assert(false, "The contract did not throw."); + + } + + module.exports = { + shouldThrow, + }; + "test/helpers/time.js": | + async function increase(duration) { + + //first, let's increase time + await web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_increaseTime", + params: [duration], // there are 86400 seconds in a day + id: new Date().getTime() + }, () => {}); + + //next, let's mine a new block + web3.currentProvider.send({ + jsonrpc: '2.0', + method: 'evm_mine', + params: [], + id: new Date().getTime() + }) + + } + + const duration = { + + seconds: function (val) { + return val; + }, + minutes: function (val) { + return val * this.seconds(60); + }, + hours: function (val) { + return val * this.minutes(60); + }, + days: function (val) { + return val * this.hours(24); + }, + } + + module.exports = { + increase, + duration, + }; + + answer: > + const CryptoZombies = artifacts.require("CryptoZombies"); + + const utils = require("./helpers/utils"); + + const time = require("./helpers/time"); + + var 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}); + expect(result.receipt.status).to.equal(true); + expect(result.logs[0].args.name).to.equal(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); + expect(newOwner).to.equal(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); + expect(newOwner).to.equal(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); + expect(newOwner).to.equal(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 time.increase(time.duration.days(1)); + await contractInstance.attack(firstZombieId, secondZombieId, {from: alice}); + expect(result.receipt.status).to.equal(true); + }) + }) +--- +ここまで、組み込みの `アサート` モジュールを使ってきた。悪くはないが、この `アサート` モジュールには有名な欠点がある- コードの可読性が良くないことだ。ありがたいことに他にも良いアサーションモジュールが存在し、 `Chai` はその中でも最も素晴らしい。 + +## Chai のアサーションライブラリ + +`Chai` はとてもパワフルだから、このレッスンの範囲では、表面をなぞるだけになる。もっと知りたければレッスンが終わった後にでもガイドを読んでみるといいだろう。 + +`Chai` に組み込まれている3種類のアサーションの型を見てみよう。 + +- _expect_: 自然な言葉の流れでアサーションを書ける: + + ```javascript + let lessonTitle = "Testing Smart Contracts with Truffle"; + expect(lessonTitle).to.be.a("string"); + ``` + +- _should_: `expect` インターフェースに似ているが、`should` のプロパティから始まる: + + ```javascript + let lessonTitle = "Testing Smart Contracts with Truffle"; + lessonTitle.should.be.a("string"); + ``` + +- _assert_: node.js にパッケージされている表記法と似たもので、いくつかテストが追加され、ブラウザと互換性がある: + + ```javascript + let lessonTitle = "Testing Smart Contracts with Truffle"; + assert.typeOf(lessonTitle, "string"); + ``` + +この章では、`expect` を使ってアサーションを改善する手法を紹介するぞ。 + +> 注: `chai` パッケージがコンピューターにインストール済みであると想定しておる。まだであれば、簡単にインストールできる: `npm -g install chai` + + +`expect` の型を使おうとすると、まずは我々のプロジェクトにインポートしなければならない: + +```javascript +var expect = require('chai').expect; +``` + +## expect().to.equal() + +これでプロジェクトに `expect` をインポートできた。2つの文字列が同じであるかをチェックするには、このようになる: + +```javascript +let zombieName = 'My Awesome Zombie'; +expect(zombieName).to.equal('My Awesome Zombie'); +``` + +話は終わりだ。`Chai` の力を借りて改善するぞ! + +# さあテストだ + +1. `expect` をプロジェクトにインポートせよ。 + +2. 先ほどの `zombieName` の例を元に、`expect` を使って次のように書き換えられる: + +```javascript +expect(result.receipt.status).to.equal(true); +``` +アリスがゾンビを所有しているかチェックするにはこうだ: + +```javascript +expect(zombieOwner).to.equal(alice); +``` + +1. `assert.equal` で書かれた箇所を全て `expect` に置き換えるんだ。見つけやすいようコメントを残しておいたぞ。 diff --git a/jp/11/14.md b/jp/11/14.md new file mode 100644 index 0000000000..84a18a0426 --- /dev/null +++ b/jp/11/14.md @@ -0,0 +1,185 @@ +--- +title: Loom上でテストする +actions: ['答え合わせ', 'ヒント'] +requireLogin: true +material: + editor: + language: javascript + startingCode: + "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/") + }, + 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/") + }, + 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" + } + } + }; + + + answer: | + 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/") + }, + network_id: "1" + }, + // Configuration for Rinkeby Network + rinkeby: { + provider: function() { + return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/") + }, + 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'; + const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); + loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10); + return loomTruffleProvider; + }, + network_id: '9545242630824' + } + }, + compilers: { + solc: { + version: "0.4.25" + } + } + }; +--- + +見事だ!さぞかし修行を重ねてきたんだろう。 + +さあ、**_Loom_** のテストネット上でテストする方法を紹介しなければ、このチュートリアルは終われない。 + +以前のレッスンを思い出して欲しい。 **_Loom_** 上のユーザは **イーサリアム** 上に比べて、はるかに高速かつガス代のかからない取引ができるんだった。これにより、ゲームやユーザー向けのDAppにはDAppチェーンが最適となる。 + +そしてこれは知っているか? **Loom** 上にデプロイすることとテストすることには違いがないのだ。やるべきことをまとめておいたから、**_Loom_** 上でテストできるようになる。軽く見ていこう。 + +## **Loom** 上でテストするための定義をTruffleに書く + +最初から始めるぞ。TruffleにLoomのテストネットへデプロイする方法を伝えよう。次のスニペットのコードを `networks` オブジェクトの中に書くんだ。 + +```javascript + 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' + } +``` + +> 注:秘密鍵を公開するなよ!お前に秘密鍵を見せたのは、あくまで雰囲気を掴ませるためだ。安全のため、秘密鍵はファイルに保存し、そこから読みだしたほうがいい。そうすれば秘密鍵を書いたファイルをうっかりGitHubにプッシュし、全ての人に公開してしまう羽目にならずにすむだろう。 + +## accounts 配列 + +_Truffle_ が **Loom** と「対話」できるようにするため、デフォルトの `HDWalletProvider` をTruffle Providerに置き換えてある。その結果、`accounts` 配列を作成してゲームをテストできるよう。プロバイダに指示しなければならない。そのためには、`LoomTruffleProvider` を `return` しているコードを置き換えるのだ: + +```javascript +return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey) +``` + + これに変更する: + +```javascript +const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); +loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10); +return loomTruffleProvider; +``` + +# さあテストだ + +1. `LoomTruffleProvider` を `return` している箇所を上記のスニペットのコードに置き換えよ。 + + +もう一つやることがある。タイムトラベルは _Ganache_ 上でテストする時に限り有効なので、テストをスキップしなければならない。すでに `x` を関数の前につけて _スキップ_ するやり方を知っておるだろうが、今回は別の方法も教えておこう。簡単に説明すると... `skip()` 関数を連ねることで _スキップ_ できるのだ: + +```javascript +it.skip("zombies should be able to attack another zombie", async () => { + //We're skipping the body of the function for brevity + }) +``` + +すでにテストをスキップするようにしておいた。よし、`truffle test --network loom_testnet`コマンドを叩くぞ。 + +コマンドを叩いたら、出力はこんな感じになる: + +```bash +Contract: CryptoZombies + ✓ should be able to create a new zombie (6153ms) + ✓ should not allow two zombies (12895ms) + ✓ should return the correct owner (6962ms) + - zombies should be able to attack another zombie + with the single-step transfer scenario + ✓ should transfer a zombie (13810ms) + with the two-step transfer scenario + ✓ should approve and then transfer a zombie when the approved address calls transferFrom (22388ms) + + + 5 passing (2m) + 1 pending + ``` + +今のところは以上だ! `CryptoZombies` スマートコントラクトのテストが完了したぞ。 diff --git a/jp/11/lessoncomplete.md b/jp/11/lessoncomplete.md new file mode 100644 index 0000000000..967e3ba2b6 --- /dev/null +++ b/jp/11/lessoncomplete.md @@ -0,0 +1,17 @@ +--- +title: Lesson Complete! +actions: ['答え合わせ', 'ヒント'] +material: + lessonComplete: 1 +--- + +ゲームをテストし終えたな。お前は並外れた存在だ! + +デモのために作ったゲームとはいえ、Solidityスマートコントラクトをテストすることは簡単なことではない。しかし今後お前のスマートコントラクトをテストするだけの準備ができたであろう! + +覚えておいて欲しいこと: + +- ゲームの関数ごとにテストを分けて作成する +- 全てを明確にラベル分けして整理する +- タイムトラベルを利用する +- ゲームやユーザー向けのDAppは、Loomにデプロイすることを考える。始めるまえに我々のドキュメントをチェックすることを強く勧めるぞ。 diff --git a/jp/index.ts b/jp/index.ts index 695aa04248..fefb725a74 100644 --- a/jp/index.ts +++ b/jp/index.ts @@ -115,6 +115,24 @@ import l7_ch9 from './7/09.md' import l7_ch10 from './7/10.md' import l7_ch11 from './7/11.md' import l7_complete from './7/lessoncomplete.md' + +// lesson11 Testing +import l11_overview from './11/00-overview.md' +import l11_ch1 from './11/01.md' +import l11_ch2 from './11/02.md' +import l11_ch3 from './11/03.md' +import l11_ch4 from './11/04.md' +import l11_ch5 from './11/05.md' +import l11_ch6 from './11/06.md' +import l11_ch7 from './11/07.md' +import l11_ch8 from './11/08.md' +import l11_ch9 from './11/09.md' +import l11_ch10 from './11/10.md' +import l11_ch11 from './11/11.md' +import l11_ch12 from './11/12.md' +import l11_ch13 from './11/13.md' +import l11_ch14 from './11/14.md' +import l11_complete from './11/lessoncomplete.md' // chapterList is an ordered array of chapters. The order represents the order of the chapters. // chapter index will be 1-based and not zero-based. First chapter is 1 @@ -235,4 +253,22 @@ export default { l7_ch11, l7_complete, ], + 11: [ + l11_overview, + l11_ch1, + l11_ch2, + l11_ch3, + l11_ch4, + l11_ch5, + l11_ch6, + l11_ch7, + l11_ch8, + l11_ch9, + l11_ch10, + l11_ch11, + l11_ch12, + l11_ch13, + l11_ch14, + l11_complete, + ], }