Smalltalk
Pharo
テスト駆動開発

テスト駆動開発でお試しする Pharo Smalltalk・第1回 はじめてのレッド、イエロー、グリーン

新訳版『テスト駆動開発』をネタに Smalltalk を学ぶ企画です。同書でケント・ベックの独特な語り口で展開される、細かなステップを演出するための 茶番 絶妙な試行錯誤 の忠実な写経はここではしません。あらかじめ、あしからず。^^;(12/24追記:なるべく忠実な写経は別に書きました→こちら

同書第Ⅰ部「多国通貨」の

  1. Dollar を定義
  2. Dollar をコピーして Franc を定義
  3. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ
  4. Sum(Expression)、Bank を定義して reduce を実現

という大まかな流れだけを大雑把になぞりながら、TDD の文脈で Smalltalk の特徴や、Pharo という処理系に組み込みの IDE の機能をご紹介します。

第1回は「はじめてのレッド、イエロー、グリーン」と題し、仮実装までを試しましょう。

使用する処理系は Pharo の最新版の 6.1 ではなく、Pharo MOOC で使われている少し古い 5.0 を使います。(追記:確認したところ、6.1 でも大丈夫そうなので既に 6.1 をインストール済みのでしたら、次の作業は飛ばして結構です。)

とりあえず、新訳版『テスト駆動開発』に出てくる「どのくらいのフィードバックが必要か」に登場するSmalltalkの例をPharoで試す - Pharo の入手とインストール、起動 のところにも書いた リンク先 をに従って、ご利用のホスト OS 向けの環境をインストール、起動を確認してください。

起動を確認したらデスクトップクリック → System Browser を選択してクラスブラウザを開いて、さっそく始めましょう。なお、起動時にデスクトップ中央に表示されている Welcome ウインドウは残しておいても閉じてしまっても構いません(閉じてしまっても WelcomeHelp open という式を Playground などに入力し Do it で評価することで再び開くことができます)。

741.png

742.png

はじめてのレッドバー

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す

とりあえず、5 USD というメッセージ式が Dollar オブジェクトを返すようにしてみましょう。

まず、すでにブラウザの下の枠(コード枠)に表示されているクラス定義のテンプレートを編集して MoneyTest クラスを定義します。

TestCase subclass: #MoneyTest
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'TDD-Money'

このように書き換えたら(あるいはこのページからコピペして置き換えたら)、コード枠内で右クリック → Accept します。

760.png

メニュー項目にも、直後に左下に表示されるヒントにもあるように、Ctrl + S(macOS の場合は CMD + S)でも同じことができます。

無事 MoneyTest が生成されると、クラスブラウザの上の段の左から二番目の枠(クラスリスト枠)に MoneyTest が現れ、その右隣のプロトコルリスト枠には no messages が現れます。

761.png

プロトコルは、クラスに定義されたメソッドを整理するためのタグのようなものです。Smalltalk ではメソッドを定義するときに必ず何かしらのプロトコルを指定する必要があります。今はメソッドが一つも無いので「no messages」と表示されています。

この no messages の部分をクリックして選択するとこれまで MoneyTest クラスの定義を表示していたコード枠の内容が切り替わり、メソッド定義のテンプレートが表示されます。

ここに #testFiveUSDReturnsADollar テストメソッドを定義します。(以降、便宜的にメソッド名やその定義の冒頭には、それが属するクラス名が分かるように クラス名 >> と書きますが、この部分はメソッド定義には必要ありません。タイプして入力、あるいは、このページからコピペする場合は >> より後の部分を使ってください。)

MoneyTest >> testFiveUSDReturnsADollar
    self assert: (5 USD isKindOf: Dollar)

入力(あるいはコピペで置き換え)できたら、クラスを定義したのと同じように Accept (Ctrl + S) します。

762.png

Author identification が現れたら適当な名前をこたえてやってください。

AuthorIdentification.png

続けてコンパイルの処理に入ると、まず「Unknown variable: Dollar please correct, or cancel:」と尋ねられます。これは Dollar クラスが定義されていないために出てくる警告です。

763.png

もうちょっと正確に書くと、Smalltalk では大文字で始まる変数はグローバル変数(クラス名を含む)かクラス変数として解釈されるのですが、これらは未定義では使えないようになっています。そこでコンパイラが、新しく定義するか、あるいは似た何かと間違い(スペルミス)なのではないか?と候補をいくつか下に列挙してくれているわけです。

ここでは Dollar を新規に定義したいので new class を選択します。すると、MoneyTest を定義したときと似たテンプレートが提示されます。追加する情報を求めて来ますが、すでに Object のサブクラスとして Dollar を定義できる記述にしてくれているので、今はこのまま OK して進めましょう。

Object subclass: #Dollar
        instanceVariableNames: '' 
        classVariableNames: ''
        category: 'TDD-Money'

InformationRequired.png

MoneyTest>>testFiveUSDReturnsADollar のコンパイルが完了し、Dollar もクラスリスト枠に追加されます。

764.png

ここで no messages だったプロトコルが as yet classified に変わるので、プロトコルリスト枠の中で 右クリック → Categorize all uncategorized を選んで、適切な tests に分類してやります。

765.png

766.png

早速、テストを走らせます。

テストクラスである MoneyTest を右クリック → run tests を選ぶと定義したテストを実行できますが、MoneyTest の名前の前にある ○印 をクリックしても同じ事ができるので以後はこちらを使います。

767.png

テストを走らせると、左下にレッドバーが現れます。同時にそれまでグレイ表示だった MoneyTest と testFiveUSDReturnsADollar の ○印 もレッドに変わり、テストが失敗していることを示してくれます。

769.png

Pharo Smalltalk の SUnit フレームワークではレッドは、エラーが発生してテストが完了できなかったことを表します。ちなみにエラーが起きずにテストは終了したが、失敗した場合はイエロー、成功はもちろんグリーンです。まずはエラーを突き止めてフィックスし、イエローを目指しましょう。

はじめてのイエローバー

エラーが起こった箇所を探すには、まず、エラーを生じているテストメソッドを指定して実行してやる必要があります。もっとも、まだテストは一つしか書いていないので testFiveUSDReturnsADollar が実行すべきメソッドです。赤い ○印 をクリックして走らせます。

770.png

すると今度は、レッドバーが表示される前に「MessageNotUnderstood: SmallInteger>>USD」と称した小さ目のウインドウ(ノーティファイアー)が現れます。このノーティファイアーが意味するのは「5 が USD なんてメッセージは知らない、と言っている」です。

MessageNotUnderstoodSmallIntegerUSD.png

知らないのであれば、教えてあげましょう。Create ボタンを押すと、SmallInteger(5 が属するクラス)とそのスーパークラスの一覧が #USD を定義する先の候補として列挙されるので、ここは無難に Number あたりを選んでやります。

771.png

続けて、新しく定義する Number>>#USD に指定するプロトコルを要求してきますが、ここでは既存のものではなく新たに下の入力欄に *TDD-Money とタイプして OK しましょう。

773.png

すると、self shouldBeImplemented という内容のスタブメソッドとして Number>>#USD が自動生成され、デバッガー内で実行を続けようとします。

MessageNotUnderstoodSmallIntegerUSD (2).png

しかし self shouldBeImplemented のままではエラーになってしまうので、この部分は削除して、メソッド名の USD だけを残し、あとの中身は空のままいったんコンパイル(Ctrl + S)してやります。

775.png

そのまま左上のクローズボタンでデバッガーは閉じてしまい、改めて MoneyTest の ○印 をクリックして走らせると、テストはエラー無しでめでたく失敗し、待望のイエローバーが現れます。

776.png

はじめてのグリーンバー

最短でグリーンを出すには、Number>>* に Dollar のインスタンスを返させればよいので、そうしましょう。

プロトコルを * に続けてパッケージ名に一致させておくと、そのプロトコルに属さないクラス(この場合 Number )のメソッド(同、USD)でも、パッケージを切り替えずに閲覧できるので便利です(その代わり、他の似たメソッドと同様の適切なプロトコルを指定できないので、それとのトレードオフになります)。

クラスリスト枠内にグレイアウトされた Number が追加されるのでクリックして選択すると右側のプロトコルリスト枠に *TDD-Money が、さらに隣のメソッドリスト枠に USD が現れるのでクリックして選択します。

777.png

デバッガー内で定義を変更したとおり、中身の無い USD メソッドがコード枠に表示されるので、下の様に追記してコンパイル(Ctrl + S)します。

USD
    ^ Dollar new

778.png

コンパイルが済んだら、MoneyTest の ○印 をクリックしてグリーンバーが出るのを確認しましょう。

779.png

おめでとう。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す

ここまでのまとめと補足

  • Pharo Smalltalk では、エラーはレッド、失敗はイエロー、成功はグリーン
  • Smalltalk では、大文字始まりはグローバル変数(クラス変数を含む)かクラス変数(クラスとサブクラス、それらのインスタンス達で共有できる変数)
  • 変数は先に宣言しておかないと(グローバル変数なら、先に作っておかないと)それらを参照する式をコンパイルできない
  • プロトコルはメソッド整理のためのタグのようなもので、必ず設定しないといけない
  • コンパイルはメソッド単位
  • メソッド定義の構文は無いのでメソッド定義テキストをクラスブラウザのコード枠でコンパイルする(クラスなどにメッセージで送る)必要がある
  • メソッドは特に指定しなければ self を返す

この時点のコード

ファイルアウト形式のソースです。

Pharo では、いったんこのコードをテキストファイルに保存してから、そのファイルを Pharo 内から Tools → File Browser で開き、Filein ボタンをクリックすると読み込めます。

Squeak でも読み込みや実行が可能なコードにもなっています。Pharo 同様に File List で開くか、デスクトップクリック → Workspace で開いたワークスペースなどにコピペし、alt/cmd + shift + g などで file it in するとクラスブラウザから閲覧したり、デスクトップクリック → Test Runner でテストも試せます(ただし使い勝手は Pharo に比べるとかなり落ちます^^;)。

!Number methodsFor: '*TDD-Money' stamp: 'sumim 11/30/2017 21:02'!
USD
    ^ Dollar new! !


Object subclass: #Dollar
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


TestCase subclass: #MoneyTest
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


!MoneyTest methodsFor: 'tests' stamp: 'sumim 11/30/2017 18:18'!
testUSDReturnsDollar
    self assert: (5 USD isKindOf: Dollar)! !