2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SmalltalkAdvent Calendar 2017

Day 13

テスト駆動開発でお試しする Pharo Smalltalk・第6回 Moneyの乗除算で通貨換算

Last updated at Posted at 2017-12-15

新訳版『テスト駆動開発』の第Ⅰ部の多国通貨をネタに Smalltalk を学ぶシリーズの第6回です。同書と似た仕様を目指しますが、好みを優先する等の都合で写経にはなっていませんのでどうぞあしからず。(12/24追記:なるべく忠実な写経は別に書きました→こちら

前回 は、Dollar、Franc を廃止して、代わりに具象クラス化した Money を使うことができるように整えました。

今回から、いよいよ当初の目的である異種通貨同士の加算の実現を目指します。ただ同書の流れからは前回まで以上に大きく外れて、換算レートのオブジェクト化(MoneyXRate, money exchange rate)とそれを用いた換算について考えてみます。

加えて次回以降の Sum についても同書で試された再帰的な実装ではなく、第17章でちょっとだけ触れられている別の実装(MoneyBag)をケント・ベックはこんな感じで考えていたのではないかな…と勝手な想像を巡らしながらこれを試してみる予定です。


#####*参考: 多国通貨実装のおおまかな流れ* 1. Dollar を定義 1. Dollar をコピーして Franc を定義 1. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ 1. **Sum(Expression)、Bank を定義して reduce を実現** ← 今ここ

同書を読んでいない方向けに補足:Sum(当シリーズでは MoneyBag)は換算前の異種通貨同士の加算(式, expression)の保持を、Bank は異種通貨間の換算レートの管理、および、Sum の換算を伴う簡約(reduce)それぞれ担当します。

10 CHF * (1 USD / 2 CHF) で USD への換算は可能では?

同書は異種通貨加算を念頭にこれを保持する Sum から発想をスタートし、簡約(reduce)に付随する処理として換算(exchange)を考えています。

しかし、簡約時に換算が必須の異種通貨加算のことはいったん忘れて、たとえば 10 CHF を USD にレート(USD/CHF = 1/2)で換算する場合、

10 CHF * (1 USD / 2 CHF) "=> 5 USD "

という Money のみの乗除算で表現するのも面白そうだということに気がつきました。この方向で掘り下げてみましょう。

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

いずれ 1 USD / 2 CHF が換算レートである MoneyXRate(厳密には~のインスタンス。他の回も含め、“~のインスタンス”は面倒なので断り無く省略していることがあります。こだわる向きはご容赦を…)を返すよう実装するのですが、その前に、数値で割った場合と同種通貨同士で割った場合は換算レートを返す必要はないので、普通に割り算の結果を返す実装にします。

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

まずテストから。

MoneyTest >> testDividedByNum
	self assert: 2 USD / 2 equals: 1 USD

996.png

MoneyTest の ○印 をクリックして全テストを走らせるとレッド(エラー)になります。

998.png

Money>> / が未定義だからなのは明らかですが、念のための確認と、その道すがら Money>> / の定義を済ませてしまいたいので、テストメソッド一覧で唯一赤くなっている testDividedByNum の ○印 をクリックしてこのテストのみを走らせます。

999.png

はたしてノーティファイアーは Money>> - の未定義を告げて来ますので、Create で作ります。

1000.png

定義先を問われるので Money を選択します。

1001.png

プロトコルについては、arithmetic とタイプして OK します。なお、/ のようなよく使うメソッドは as yet classified(未分類)のまま OK しておいて、あとでプロトコルリスト枠の右クリック → Categorize all uncategorized で自動分類も可能です。

1002.png

self shouldBeImplemented という内容のスタブメソッドが定義され、そのコンテキストでデバッガーが開くので、当該メソッドをテストをイエロー(失敗)に変えるために空のメソッド(あるいは self を返すメソッド)に書き換えてコンパイル(Accept)します。なお、仮引数は aMoney から divisor に変更しました。なお、ここで提示するコードの冒頭の クラス名 >> の部分はメソッドのコードの入力(あるいはコピペ)の際は含める必要はありません。念のため。

Money >> / divisor

1003.png

デバッガーを閉じて、再び MoneyTest の全テストを走らせる(○印 クリック)と、バーはレッド(エラー)からイエロー(失敗)に変わっているはずです。

1004.png

改めて、Money>> - を呼び出し、今度はテストがグリーン(成功)になるように 1 USD を返すコードに内容を変えてコンパイルします(仮実装)。^ は Smalltalk ではリターンです。

Money >> / divisor
	^ 1 USD

1006.png

テストの結果がグリーンになることを確認して、重複を排除します(リファクタリング)。

これからいろいろな divisor が来るので、とりあえず予防的に Money 以外が divisor に来た場合はエラーになるようにもしておきましょう。

Money >> / divisor
	divisor isNumber
		ifTrue: [ ^ amount / divisor perform: currency ].
	self error: 'unsupported divisor'

1007.png

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

次は同種通貨同士の割り算です。

MoneyTest >> testDividedBySameCurrency
	self assert: 2 USD / 1 USD equals: 2

テストはエラーで、testDivideSameCurrency のみに走らせて確認すると、先ほど設置したエラーだと分かります。

Error; unsupported divisor.png

Mnney>> / を Number の divisor にも対応できるようにこのように変えます。

Money >>/ divisor
	divisor isNumber
		ifTrue: [ ^ amount / divisor perform: currency ].
	divisor currency == currency
		ifTrue: [ ^ amount / divisor amount ].
	self error: 'unsupported divisor'

1009.png

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

isNumber を真似て isMoney を仕込む

isNumber というのは Ojbect と Number に次のようにシンプルな実装で実現されている testing メソッドです。Money>> / コード内の isNumber を選択して 右クリック → Code search ... → implementors of it で定義先を一覧として呼び出せます。

1010.png

Object >> isNumber
     "Overridden to return true in Number, natch"
     ^ false
Number >> isNumber
     ^ true

Implementors of isNumber [2].png

オープンクラスかそれに準ずる機能(組み込みのを含めて既存のクラスに手を加えられる)を持つ言語限定で、なおかつ静的型ファンからすれば、動的型言語のつまらないバッドノウハウかも…ですが、この testing メソッドは、いちいち ~ class == クラス名 などとしなくてよいので便利です。(ただし乱用は禁物か。)

あると何かと便利そうなので Money にも用意してみましょう。まずテストを書きます。

MoneyTest >> testIsMoney
	self deny: 1 isMoney.
	self assert: 1 USD isMoney

MoneyTest の全テストを走らせるとエラーになるので、赤くなった testIsMoney のみ走らせて isMoney を Create します。デバッガー内からは複数箇所に同時には同名メソッドは定義できないので、とりあえずここでは定義先を Object、プロトコルは(本来は testing なのですが、現在作業中の TDD-Money パッケージに含めたいので)*TDD-Money にします。あたまの * をお忘れなく。

1012.png

Object >> isMoney
	^ false

デバッガーを閉じて、クラスブラウザを見ると、クラスリスト枠に Object が追加され、さらに *TDD-Money プロトコルに isMoney も見つかるはずです。

1013.png

Money>>isMoney も忘れずに作成しておきます。こちらは本来の testing プロトコルに分類します。

まず Object から Money に切り替えて、そのプロトコルリスト枠で 右クリック → Add Protocol... を選択し、testing とタイプして入力して OK を押します。

1014.png

改めて下のコード枠に次のコードを入力してコンパイル(Accept)します。

Money >> isMoney
	^ true

これでテストは通るはずです。

1015.png

異種通貨同士の除算で MoneyXRate を生成

さて、次は本題の異種通貨同士の除算の場合です。

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

テストを書きます。

MoneyTest >> testDivideDifferentCurrencyReturnsMoneyXRate
	| aMoneyXRate |
	aMoneyXRate := 1 USD / 2 CHF.
	self assert: aMoneyXRate class equals: MoneyXRate

コンパイルしようとすると、MoneyXRate が未宣言であることをコンパイラーに咎められるので Define new class を選択します。MoneyXRate のクラス定義式のテンプレートが提示されますがそのまま OK します。

Object subclass: #MoneyXRate
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'

MoneyXRate がクラスブラウザに追加され、testDivideDifferentCurrencyReturnsMoneyXRate のコンパイルが通ったら、MoneyTest の全テストを走らせます。

結果はエラーですが、Money>> / のサポートしない divisor を告げるために設置したものなので問題ありません。

1016.png

テストを成功させる仮実装に変えて Money>> / をコンパイルしなおします。

Money >> / divisor
	divisor isNumber
		ifTrue: [ ^ amount / divisor perform: currency ].
	divisor isMoney
		ifFalse: [ ^ self error: 'unsupported divisor' ].
	^ divisor currency == currency
		ifTrue: [ amount / divisor amount ]
		ifFalse: [ MoneyXRate new ]

1017.png

テストの結果はグリーンです。

さて、MoneyXRate には「どの通貨からどの通貨への換算時か?(fromToCurrency)」と「そのレートは?(xRate)」の2つの情報を保持させます。

前回同様、set で始まるコンストラクターパラメーターメソッド(ベパp27)を private プロトコルに設け、MoneyXRate のインスタンス生成時に使いましょう。

MoneyXRate >> setFromToCurrency: anAssociation xRate: aNumber
	fromToCurrency := anAssociation.
	xRate := aNumber

この MoneyXRate>>setFromToCurrency:xRate: をコンパイル(Accept)をしようとすると、fromToCurrency、xRate それぞれについて未宣言であることをコンパイラーに指摘されますので、つど Declare new instance variable を選んで宣言を代行してもらいます。

1018.png

MoneyXRate>>setFromToCurrency:xRate: のコンパイルが通ったら、それをコールするように Money>> / も書き換えます。

Money >> / divisor
	divisor isNumber
		ifTrue: [ ^ amount / divisor perform: currency ].
	divisor isMoney
		ifFalse: [ ^ self error: 'unsupported divisor' ].
	^ divisor currency == currency
		ifTrue: [ amount / divisor amount ]
		ifFalse: [ MoneyXRate new
				setFromToCurrency: divisor currency -> currency
				xRate: amount / divisor amount ]

1020.png

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

MoneyXRate を使った通貨換算

では次にてこの MoneyXRate を使って通貨換算をしてみましょう。

テストを書きます。

MoneyTest >> testExchangeMoney
	| aMoneyXRate |
	aMoneyXRate := 1 USD / 2 CHF.
	self assert: 10 CHF * aMoneyXRate equals: 5 USD

1021.png

テストはエラー(レッド)です。改めて testExchangeMoney を走らせて原因を探ると、Money>> * からおかしな方向へ処理が進んでしまっていることが分かります。multiplier が Number 以外を想定していないこと(エラーを出すことを含め)が原因のようですね。

1022.png

Money>> * を書き換えて対処します。仮実装も済ませてしまいましょう。

Money >> * multiplier
	multiplier isNumber ifTrue: [ ^ (amount * multiplier) perform: currency ].
	^ 5 USD

1023.png

テストを走らせてグリーンを確認してから重複を排除します。換算したい通貨(currency)と換算レートの分母(内部的には fromToCurrency の key)が一致しているかもここでチェックしておく必要がありそうです。

Money >> * multiplier
	multiplier isNumber
		ifTrue: [ ^ amount * multiplier perform: currency ].
	self assert: multiplier fromCurrency == currency.
	^ amount * multiplier xRate perform: multiplier toCurrency

未定義のメソッドをいろいろコールしてしまっているので、テストは失敗します。

1024.png

赤くなった testExchangeMoney のみを走らせて、未定義を指摘されたメソッドを順に定義してゆきましょう。

未定義を指摘されるのは順に fromCurrency 、xRate 、toCurrency の3つのメソッドです。いずれも定義先はもちろん MoneyXRate で、プロトコルは accessing にして次のように定義します。

MoneyXRate >> fromCurrency
	^ fromToCurrency key
MoneyXRate >> xRate
	^ xRate
MoneyXRate >> toCurrency
	^ fromToCurrency value

一つの定義が終わったら、デバッガーを閉じて再度 testExchangeMoney を走らせてもよいですし、デバッガーを閉じずにそのまま Proceed(デバッガーウインドウのタイトルバーすぐ下に並んでいるボタン列の左端にある)しても構いません。

toCurency 定義後に勢い余って Proceed してしまうとスレッド(テストの run )が終了し、あらかじめ用意されていたレッドバーが出てしまいますが、もう一度 testExchangeMoney か MoneyTest の ○印 をクリックすればちゃんとグリーンが確認できます。

1025.png

どうやら Pharo の SUnit は、ここで多用しているいわゆる“デバッガー駆動開発”でレッドやイエローが修正されることを想定していないようです。なんだかちょっと残念ですね。

目指す Smalltalk 式を、そこでコールするメソッドが定義済みか否かは問わず記述・評価し、その後ノーティファイアーで未定義を指摘されるメソッドを順にデバッガー内で定義 → Proceed を繰り返し、最後にエラーが全て解消してデバッガーを抜けるとコードが完成している…、そんな Smalltalk お家芸とも言える特殊な開発サイクル体験をこう呼ぶことがあります。

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

MoneyXRate>>printOn: 、 = 、hash 、の整備

最後に MoneyXRate を値として扱うために再定義すべきメソッドをいくつか片付けておきましょう。

まず printOn: の再定義から済ませましょう。プロトコルは printing です。

MoneyXRate >> printOn: aStream
	aStream
		print: xRate;
		space;
		nextPutAll: self toCurrency;
		nextPutAll: ' / ';
		print: 1;
		space;
		nextPutAll: self fromCurrency

1026.png

これでノーティファイアなどの表示が分かりやすくなります。

改めて、MoneyXRate>> = 、hash の定義は以下のとおりです。いずれもプロトコルは comparing です。

MoneyXRate >> = other
	^ other class == self class
		and: [ other fromToCurrency = fromToCurrency and: [ other xRate = xRate ] ]
MoneyXRate >> hash
	^ fromToCurrency hash bitXor: xRate hash

ついでというわけではないですが、第3回で気になった Money>>hash も直してしまいましょう。

Money >> hash
	^ currency hash bitXor: amount hash

#ここまでのまとめと補足

  • 演算子のオーバーロード(Smalltalk には演算子というものはないので、ごく普通に二項メソッドの追加というだけですが…)はスポっとはまると気持ちいい反面、落とし穴もあるので使うときは慎重にしないといけないようです。
  • 調子に乗って通貨の単位を含めた分数(MoneyFraction?)まで実装したくなりますが、物理計算のように 1 / USD2 みたいな値に意味ががあるわけではないのと、典型的 YAGNI なのでやめておくのが吉のようです。
  • Pharo の SUnit フレームワークは、意外や Smalltalk のお家芸である“デバッガー駆動開発”を想定していないため、せっかくの作業の流れを分断される辛みがある(正しい色のバーを出すには、デバッガーを閉じてから再度テストを走らせないといけない)。
  • isNumber のような testing メソッドは、実装がとてもシンプルで重宝しますが、多用しないよう要注意です。
  • パッケージに含まれていないクラスへのメソッド追加でも、プロトコルを「*パッケージ名」にしておくことでパッケージに含めることが可能。反面、本来の相応しいプロトコルにできないのでメソッドを探すときに少し困ります。
  • MoneyXRateの等価性テストしていなくて、MoneyXRate>>fromToCurrency の実装を忘れているのに気付きませんでした。すみません。(次回やります)

#この時点のコード
ファイルアウト形式のソースです。(この形式で Smalltalk のソースを読むことはあまり推奨されません。為念)

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 12/7/2017 18:16'!
USD
	^ Money amount: self currency: #USD! !


!Number methodsFor: '*TDD-Money' stamp: 'sumim 12/7/2017 18:16'!
CHF
	^ Money amount: self currency: #CHF! !


!Object methodsFor: '*TDD-Money' stamp: 'sumim 12/14/2017 18:03'!
isMoney
	^ false! !


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


!Money methodsFor: 'accessing' stamp: 'sumim 12/7/2017 17:32'!
currency
	^ currency! !


!Money methodsFor: 'accessing' stamp: 'sumim 12/5/2017 17:28'!
amount
	^ amount! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/14/2017 18:06'!
* multiplier
	multiplier isNumber
		ifTrue: [ ^ amount * multiplier perform: currency ].
	self assert: multiplier fromCurrency == currency.
	^ amount * multiplier xRate perform: multiplier toCurrency! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/14/2017 18:05'!
/ divisor
	divisor isNumber
		ifTrue: [ ^ amount / divisor perform: currency ].
	divisor isMoney
		ifFalse: [ ^ self error: 'unsupported divisor' ].
	^ divisor currency == currency
		ifTrue: [ amount / divisor amount ]
		ifFalse: [ MoneyXRate new
				setFromToCurrency: divisor currency -> currency
				xRate: amount / divisor amount ]! !


!Money methodsFor: 'comparing' stamp: 'sumim 12/7/2017 17:50'!
= other
	^ other currency == currency and: [ other amount = amount ]! !


!Money methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
hash
	^ currency hash bitXor: amount hash! !


!Money methodsFor: 'printing' stamp: 'sumim 12/7/2017 18:08'!
printOn: aStream
	aStream print: amount; space; nextPutAll: currency! !


!Money methodsFor: 'private' stamp: 'sumim 12/7/2017 17:41'!
setAmount: aNumber currency: aSymbol
	amount := aNumber.
	currency := aSymbol! !


!Money methodsFor: 'testing' stamp: 'sumim 12/14/2017 18:04'!
isMoney
	^ true! !


!Money class methodsFor: 'instance creation' stamp: 'sumim 12/7/2017 18:15'!
amount: aNumber currency: aSymbol
	^ self new setAmount: aNumber currency: aSymbol; yourself! !


Object subclass: #MoneyXRate
	instanceVariableNames: 'fromToCurrency xRate'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:07'!
toCurrency
	^ fromToCurrency value! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:06'!
fromCurrency
	^ fromToCurrency key! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:06'!
xRate
	^ xRate! !


!MoneyXRate methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
= other
	^ other class == self class
		and: [ other fromToCurrency = fromToCurrency and: [ other xRate = xRate ] ]! !


!MoneyXRate methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
hash
	^ fromToCurrency hash bitXor: xRate hash! !


!MoneyXRate methodsFor: 'printing' stamp: 'sumim 12/14/2017 18:08'!
printOn: aStream
	aStream
		print: xRate;
		space;
		nextPutAll: self toCurrency;
		nextPutAll: ' / ';
		print: 1;
		space;
		nextPutAll: self fromCurrency! !


!MoneyXRate methodsFor: 'private' stamp: 'sumim 12/14/2017 18:05'!
setFromToCurrency: anAssociation xRate: aNumber
	fromToCurrency := anAssociation.
	xRate := aNumber! !


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


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:28'!
testCurrency
	self assert: 5 USD currency equals: #USD.
	self assert: 5 CHF currency equals: #CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:04'!
testDivideDifferentCurrencyReturnsMoneyXRate
	| aMoneyXRate |
	aMoneyXRate := 1 USD / 2 CHF.
	self assert: aMoneyXRate class equals: MoneyXRate! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:00'!
testDividedByNum
	self assert: 2 USD / 2 equals: 1 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:02'!
testDividedBySameCurrency
	self assert: 2 USD / 1 USD equals: 2! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/5/2017 12:55'!
testEquality
	self assert: 5 USD = 5 USD.
	self assert: 5 USD ~= 6 USD.
	self assert: 5 CHF = 5 CHF.
	self assert: 5 CHF ~= 6 CHF.
	self assert: 5 USD ~= 5 CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:05'!
testExchangeMoney
	| aMoneyXRate |
	aMoneyXRate := 1 USD / 2 CHF.
	self assert: 10 CHF * aMoneyXRate equals: 5 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:30'!
testFiveCHFReturnsAMoney
	self assert: 5 CHF class equals: Money! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:30'!
testFiveUSDReturnsAMoney
	self assert: 5 USD class equals: Money! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:03'!
testIsMoney
	self deny: 1 isMoney.
	self assert: 1 USD isMoney! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/5/2017 12:56'!
testMultiplication
	self assert: 5 USD * 2 equals: 10 USD.
	self assert: 5 CHF * 2 equals: 10 CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:28'!
testPrintString
	self assert: 5 USD printString equals: '5 USD'.
	self assert: 5 CHF printString equals: '5 CHF'! !
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?