私はテスト駆動開発 (TDD: Test-Driven Development) の熱烈な支持者です。Neal Ford (『The Productive Programmer』の著者) 曰く、「テストされていないコードを書くことは、プロとして無責任なことです」(「参考文献」を参照)。Michael Feathers (『Working Effectively with Legacy Code』の著者) は「レガシー・コード」を、対応するテストが関連付けられていないソフトウェアと定義しています。この定義に含まれている意味は、テストを行わずにコードを作成するのは時代遅れのプラクティスであるということです。私は常々、プロジェクトには 1 ポンドの本番コードにつき、2 ポンドのテスト・コードが必要だと言っています。
「Grails をマスターする」では今まで Grails のコア機能を活用することに重点を置いていたため、この連載ではまだ TDD について取り上げたことはありませんでした。インフラストラクチャー・コード (つまり、自分で作成したのではない既存のコードのこと) のテストには多少なりとも価値がありますが、私の場合、実際にテストすることはめったにありません。私は、Grails が POGO (Plain Old Groovy Object) を正しく XML に変換すること、つまり trip.save() を呼び出すとTrip が確実にデータベースに保存されることを信じています。テストが実際に価値ある提案となってくるのは、独自のカスタム・コードを検証する段階です。複雑なアルゴリズムを作成する場合には、1 つもしくはそれ以上の補足的なユニット・テストも準備して、アルゴリズムがその意図通りに機能することを証明しなければなりません。そこで今回の記事では、アプリケーションのテストを簡単に実行できるようにする Grails の機能を説明します。
テストを始めるためにまず、新しいドメイン・クラスを導入します。このクラスに最終的に備わるカスタム機能は、テストを用意しない限り、本番には組み込めません。まず、grails create-domain-class HotelStay と入力してください (リスト 1 を参照)。
リスト 1.
HotelStay クラスの作成
$ grails create-domain-class HotelStay
Environment set to development
[copy] Copying 1 file to /src/trip-planner2/grails-app/domain
Created Domain Class for HotelStay
[copy] Copying 1 file to /src/trip-planner2/test/integration
Created Tests for HotelStay
|
リスト 1 を見るとわかるように、Grails は自動的に、grails-app/domain ディレクトリーに空のドメイン・クラスを作成し、さらに test/integration ディレクトリーに空の testSomething() メソッドが含まれる GroovyTestCase クラスも作成します (ユニット・テストと結合テストの違いについての詳細は、この後説明します)。リスト 2 に、空の HotelStay クラスと、このクラスに生成されたテストを記載します。
リスト 2. 空のクラスと生成されたテスト
class HotelStay {
}
class HotelStayTests extends GroovyTestCase {
void testSomething() {
}
}
|
GroovyTestCase は JUnit 3.x ユニット・テストを覆う薄い Groovy のファサードです。JUnit の TestCase について十分理解していれば、GroovyTestCase がどのように機能するかについてもおわかりのはずです。いずれのテスト・ケースも、コードの実行内容をアサートすることによってそのコードをテストします。JUnit にはさまざまなアサーション・メソッド (assertEquals、assertTrue、assertNull の他、多数) が組み込まれており、これらのアサーション・メソッドを使用して「このコードはこの内容を実行するはずである」とプログラムで表現することができます。
リスト 3 のコードを grails-app/domain/HotelStay.groovy と test/integration/HotelStayTests.groovy に追加してください。
リスト 3. 単純なテスト
class HotelStay{
String hotel
}
class HotelStayTests extends GroovyTestCase {
void testSomething(){
HotelStay hs = new HotelStay(hotel:"Sheraton")
assertEquals "Sheraton", hs.hotel
}
}
|
リスト 3 は、前に述べた、まさにあまり価値のない Grails インフラストラクチャーのテストだということは認識しています。皆さんは、Grails がこのアクションを正しく実行すると信用しているはずなので、これは誤ったテストを作成する例としてはぴったりです。可能な限り単純なテストを作成して、そのテストが実行する様子を監視できるという点で、この記事の目的に役立ちます。
すべてのテストを実行するには grails test-app と入力し、この単一のテストを実行するには grails test-app HotelStay と入力します (「Convention over Configuration (設定より規約)」のおかげで、Tests という接尾辞は除外することができます)。どちらのコマンドを入力するにせよ、コマンド・プロンプトでリスト 4 の出力が表示されます (重要な機能を強調するために、多くの不要な部分は削除していることに注意してください)。
リスト 4. テストの実行による出力
$ grails test-app
Environment set to test
No tests found in test/unit to execute ...
-------------------------------------------------------
Running 1 Integration Test...
Running test HotelStayTests...
testSomething...SUCCESS
Integration Tests Completed in 253ms
-------------------------------------------------------
Tests passed. View reports in /src/trip-planner2/test/reports
|
上記の出力から、4 つの重要なことが行われていることがわかります。
- ご覧のように、
environmentはtestに設定されます。これは、conf/DataSource.groovy ファイルのtestブロックに含まれるデータベース設定が作用することを意味します。 - test/unit 内のスクリプトが実行されます。ユニット・テストはまだ 1 つも作成していないので、ユニット・テストが検出されなかったのは当然のことです。
- test/integration 内のスクリプトが実行されます。HotelStayTests.groovy スクリプトからの出力を見ると、その横に大きく
SUCCESSと示されています。 - スクリプトが一連のレポートの配置場所を示します。
Web ブラウザーで /src/trip-planner2/test/reports/html/index.html を開くと、実行されたすべてのテストのレポートが表示されます (図 1 を参照)。
図 1. JUnit の最上位レベルのサマリー・レポート
HotelStayTests リンクをクリックすると、doSomething() テストが表示されます (図 2 を参照)。
図 2. JUnit のクラス・レベルのレポート
テストが何らかの理由で失敗すると、コマンド・プロンプト出力と HTML レポート (図 3 を参照) の両方によってテストの失敗が通知されます。
図 3. 失敗した JUnit テスト
最初の単純なテストが機能するようになったので、今度はそれよりも現実的なテストを作成します。例えば、HotelStay クラスに Date checkIn と Date checkOut という 2 つのフィールドがあるとします。ユーザーの要件では、toString メソッドの出力は Hilton (Wednesday to Sunday) のようにならなければなりません。日付を正しいフォーマットにするのは、java.text.SimpleDateFormat クラスのおかげで簡単なので、この場合に作成するテストは SimpleDateFormat が正しく機能することを検証するためのものではありません。このテストは、toStringメソッドが期待通りに機能することを検証し、ユーザーの要件を満たしていることを証明するという 2 つの目的を果たすように作成します。
リスト 5 のコードを HotelStay.groovy と HotelStayTests.groovy に入力してください。
リスト 5.
assertToString の使用
import java.text.SimpleDateFormat
class HotelStay {
String hotel
Date checkIn
Date checkOut
String toString(){
def sdf = new SimpleDateFormat("EEEE")
"${hotel} (${sdf.format(checkIn)} to ${sdf.format(checkOut)})"
}
}
import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase {
void testSomething(){...}
void testToString() {
def h = new HotelStay(hotel:"Hilton")
def df = new SimpleDateFormat("MM/dd/yyyy")
h.checkIn = df.parse("10/1/2008")
h.checkOut = df.parse("10/5/2008")
println h
assertToString h, "Hilton (Wednesday to Sunday)"
}
}
|
grails test-app と入力して、この 2 つ目のテストにパスすることを確認します。
testToString メソッドは、GroovyTestCase が新しく仲間に入れたアサーション・メソッドの 1 つ、assertToString を使用します。JUnit assertEquals メソッドを使用したとしても確かに同じ結果が得られますが、assertToString のほうが表現力には優れています。テストの目的は、テスト・メソッドの名前と最終アサーションを見れば一目瞭然です (「参考文献」に、assertArrayEquals、assertContains、assertLength をはじめ、GroovyTestCase がサポートするアサーションの全リストへのリンクを記載しています)。
これまでは、HotelStay ドメイン・クラスとの対話はプログラムで行ってきました。ここでリスト 6 に記載する HotelStayController を追加して、Web ブラウザーでもテスト・クラスを操作できるようにします。
リスト 6.
HotelStayController のソース
class HotelStayController {
def scaffold = HotelStay
}
|
create フォームには、微妙な UI の調整が必要です。デフォルトでは、日付のフィールドには日、月、年、時、分が含まれます (図 4 を参照)。
図 4. デフォルトによる日付と時刻両方の表示
この場合、日付のフィールドのタイムスタンプの部分は無視しても問題ありません。そこで、grails generate-views HotelStay と入力し、図 5 に示す変更後の UI を作成するため、iews/hotelStay/create.gsp と views/hotelStay/edit.gsp の両方で <g:datePicker> 要素に precision="day" を追加します。
図 5. 日付のみの表示
有効に機能する HotelStay がサーブレット・コンテナーで実行されるようになったので、次のセクションに移る準備は万端です。早速、ユニット・テストまたは結合テストのどちらを実行するかを検討してみましょう。
前に述べたように、Grails では 2 つの基本的なテスト・タイプ、ユニット・テストと結合テストをサポートします。この 2 つに構文上の違いはありません。どちらも GroovyTestCase として作成し、同じアサーションを使用します。この 2 つの違いは、そのテストの意味です。ユニット・テストはクラスを単独でテストするように意図されている一方、結合テストでは稼働中の完全な環境でクラスをテストすることができます。
率直に言って、すべての Grails テストを結合テストとして作成するのでもまったく構いません。Grails のcreate-* コマンドはいずれも対応する結合テストを生成するので、大部分の開発者たちはただ単にそれによって生成された結合テストを使用しています。この後わかるように、テスト内容の大部分は完全な環境が稼働状態になっていることが条件となるので、結合テストはかなり有効なデフォルトと言えます。
コアではない Grails クラスをテストする場合には、ユニット・テストが最適です。ユニット・テストを作成するには、grails create-unit-test MyTestUnit と入力します。テスト・スクリプトは個別のパッケージに作成されるわけではないため、ユニット・テストと結合テストには固有の名前を付ける必要があります。これを怠ると、リスト 7 に記載する不愉快なエラー・メッセージが表示されることになります。
リスト 7. 結合テストとユニット・テストの名前が同じ場合に表示されるエラー・メッセージ
The sources
/src/trip-planner2/test/integration/HotelStayTests.groovy and
/src/trip-planner2/test/unit/HotelStayTests.groovy are
containing both a class of the name HotelStayTests.
@ line 3, column 1.
class HotelStayTests extends GroovyTestCase {
^
1 error
|
結合テストにはデフォルトで Tests という接尾辞が付くため、ユニット・テストにはすべて UnitTests という接尾辞を付けて、固有の名前になるようにします。
次のユーザー要件では、Hotel フィールドはブランクのままにすることはできないと規定しています。この要件は、Grails の組み込み検証フレームワークを使えば簡単に対応することができます。リスト 8 に示すように、static constraints ブロックを HotelStay に追加してください。
リスト 8.
HotelStay への static constraints ブロックの追加
class HotelStay {
static constraints = {
hotel(blank:false)
checkIn()
checkOut()
}
String hotel
Date checkIn
Date checkOut
//the rest of the class remains the same
}
|
grails run-app と入力します。Hotel フィールドに何も入力しないで HotelStay を作成しようとすると、図 6 のエラー・メッセージを受け取ります。
図 6.ブランクのフィールドに対するデフォルトのエラー・メッセージ
ユーザーはこの機能に満足するに違いありませんが、デフォルトのエラー・メッセージではユーザーを感心させるまでには至りません。そこで、ユーザーの希望内容を少し変更して、Hotel フィールドはブランクのままにできないこと、そしてフィールドがブランクの場合には「Please provide a hotel name」というエラー・メッセージを表示することを要件とします。
カスタム String と同じくらいに単純なものであっても、このようにカスタム・コードを追加する場合には、テストを追加しなければなりません (当然のことながら、カスタム・コードが関係しないとしても、ユーザー要件を満たしていると証明するテストを作成するのが無難です)。
grails-app/i18n/messages.properties を開いて、hotelStay.hotel.blank=Please provide a hotel name を追加してください。ブラウザーでブランクの Hotel フィールドをサブミットすると、このカスタム・メッセージが表示されるはずです (図 7 を参照)。
図 7. カスタム検証エラー・メッセージの表示
HotelStayTests.groovy に新しいテストを追加して、ブランクに対する検証が機能することを確認します (リスト 9 を参照)。
リスト 9. 検証エラーのテスト
class HotelStayTests extends GroovyTestCase {
void testBlankHotel(){
def h = new HotelStay(hotel:"")
assertFalse "there should be errors", h.validate()
assertTrue "another way to check for errors after you call validate()", h.hasErrors()
}
//the rest of the tests remain unchanged
}
|
前に、生成されるコントローラーではドメイン・クラスごとに save() メソッドが追加されると説明しました。ここでも save() を呼び出すことはできますが、この場合、新しいクラスをデータベースに保存したいわけではありません。検証が機能するかどうかを知りたいだけです。そこで役立つのが validate() メソッドです。このメソッドは、検証が失敗した場合には false を返し、成功した場合には true を返します。
hasErrors() メソッドも、テストには貴重なメソッドです。save() または validate() を呼び出した後、hasErrors() によって、検証エラーがあったかどうかを事後に確認することができます。
リスト 10 は、その他の貴重な検証メソッドをいくつか組み込んだ testBlankHotel() の拡張バージョンです。
リスト 10. 検証エラーの拡張テスト
class HotelStayTests extends GroovyTestCase {
void testBlankHotel(){
def h = new HotelStay(hotel:"")
assertFalse "there should be errors", h.validate()
assertTrue "another way to check for errors after you call validate()", h.hasErrors()
println "\nErrors:"
println h.errors ?: "no errors found"
def badField = h.errors.getFieldError('hotel')
println "\nBadField:"
println badField ?: "hotel wasn't a bad field"
assertNotNull "I'm expecting to find an error on the hotel field", badField
def code = badField?.codes.find {it == 'hotelStay.hotel.blank'}
println "\nCode:"
println code ?: "the blank hotel code wasn't found"
assertNotNull "the blank hotel field should be the culprit", code
}
}
|
クラスが検証に失敗したと確信した場合には、getErrors() メソッド (ここでは、Groovy のショートカットであるゲッター構文のおかげで errors に省略されています) を呼び出して org.springframework.validation.BeanPropertyBindingResult を返すことができます。GORM が Hibernate を覆う薄い Groovy ファサードであるのと同じく、Grails 検証の中身も単なる Spring 検証に過ぎません。
println 呼び出しの結果はコマンドラインではわかりませんが、HTML レポートに表示されます (図 8 を参照)。
図 8. テストによる
println 出力の表示
HotelStayTests レポートの右下隅に表示された System.out リンクをクリックしてください。
リスト 10 の愛情を込めて名付けられた Elvis 演算子 (頭を横に傾けてみてください。Elvis のオールバックと 2 つの目が見えませんか?) は、Groovy の 3 項演算子のショートカットです。?: の左側のオブジェクトがヌルの場合、右側の値が代わりに使用されます。
Hotel フィールドを「Holiday Inn」に変更してテストを再度実行してください。今度は HTML レポートに別の Elvis 出力が表示されます (図 9 を参照)。
図 9. テスト出力での Elvis の確認
Elvis を確認した後は、忘れずにHotel フィールドをブランクに戻して、壊れたテストを配置したままにしないでください。
checkIn と checkOut には他にもまだ検証エラーが表示されますが、心配には及びません。このテストに関する限り、これらの検証エラーについては無視して問題ありません。ただしこれは、エラーの有無をテストするだけでなく、特定のエラーがスローされることを確認しなければならないということの説明にはなります。
私がアサートしているテキストは、カスタム・エラー・メッセージと完全には一致していない点に注目してください。前回 (toString の出力をテストしたとき) はストリングの一致に配慮したのに、どうして今回はそうでないのかと言うと、前回のテストでの核心は toString メソッドの出力であったのに対し、今回は Grails がメッセージを正しくレンダリングすることよりも、検証コードが実行されることを確認することのほうが重要だからです。これはまさしく、テストは科学というよりは芸術であることを証明しています (正確なメッセージ出力を検証するとしたら、Canoo WebTest や ThoughtWorks Selenium などの Web 層テスト・ツールを使っていたはずです)。
次に取り組むユーザー要件は、checkOut の日付が checkIn の日付より前にならないことを確実にするというものです。この要件に対処するには、カスタム検証を作成する必要があります。そしてカスタム・コードを作成する場合には、当然そのコードのテストも必要となります。
リスト 11 のカスタム検証コードを static constraints ブロックに追加してください。
リスト 11. カスタム検証
class HotelStay {
static constraints = {
hotel(blank:false)
checkIn()
checkOut(validator:{val, obj->
return val.after(obj.checkIn)
})
}
//the rest of the class remains the same
}
|
val 変数は現行のフィールドです。obj 変数は現行の HotelStay インスタンスを表します。Groovy は before() メソッドと after() メソッドをすべての Date オブジェクトに追加するため、この検証では単に after() メソッド呼び出しの結果だけを返します。checkOut が checkIn の後であれば、検証は true を返し、そうでなければ false を返してエラーをトリガーします。
ここで grails run-app と入力して、checkIn の日付より checkOut の日付を前に設定した場合、新しい HotelStay を作成できないことを確認してください (図 10 を参照)。
図 10. デフォルトのカスタム検証エラー・メッセージ
grails-app/i18n/messages.properties を開き、checkOut フィールドのカスタム検証メッセージとして hotelStay.checkOut.validator.invalid=Sorry, you cannot check out before you check in を追加します。
messages.propertiesファイルを保存した後、誤った形式の HotelStay をもう一度保存してみてください。すると、図 11 に示すエラー・メッセージが表示されます。
図 11.カスタム検証エラー・メッセージ
今度はリスト 12 に記載するテストを作成します。
リスト 12. カスタム検証のテスト
import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase {
void testCheckOutIsNotBeforeCheckIn(){
def h = new HotelStay(hotel:"Radisson")
def df = new SimpleDateFormat("MM/dd/yyyy")
h.checkIn = df.parse("10/15/2008")
h.checkOut = df.parse("10/10/2008")
assertFalse "there should be errors", h.validate()
def badField = h.errors.getFieldError('checkOut')
assertNotNull "I'm expecting to find an error on the checkOut field", badField
def code = badField?.codes.find {it == 'hotelStay.checkOut.validator.invalid'}
assertNotNull "the checkOut field should be the culprit", code
}
}
|
対処しなければならないユーザー要件は残り 1 つとなりました。create ビューと edit ビューの checkIn と checkOut からタイムスタンプの部分を削除することには成功しましたが、図 12 を見るとわかるように、list ビューと show ビューの日付形式はまだ誤ったままです。
図 12. 日付の (タイムスタンプが含まれる) デフォルトの Grails 出力
最も簡単な対処方法は、新しい TagLib を定義することです。Grails にはすでに <g:formatDate> タグが定義されているので、この既存のタグを利用することもできますが、独自のカスタム・タグを作成すること自体、極めて簡単な作業です。そこで、<g:customDateFormat> タグを以下のように 2 通りの方法で使用できるようにします。
一方の <g:customDateFormat> タグの形は、Date をラップして、あらゆる有効な SimpleDateFormat パターンを許容するカスタム・フォーマット属性を受け入れるというものです。
<g:customDateFormat format="EEEE">${new Date()}</g:customDateFormat>
|
最も一般的な使用事例は米国式の「MM/dd/yyyy」フォーマットで日付を返すことなので、他に何も指定しない場合には、この形式をデフォルトに設定することにします。
<g:customDateFormat>${new Date()}</g:customDateFormat>
|
ユーザーの要件を理解できたところで、リスト 13 に示すように grails create-tag-lib Date と入力して、まったく新しい DateTagLib.groovy ファイルとそれに対応する DateTagLibTests.groovy ファイルを作成してください。
リスト 13. 新しい
TagLib の作成$ grails create-tag-lib Date [copy] Copying 1 file to /src/trip-planner2/grails-app/taglib Created TagLib for Date [copy] Copying 1 file to /src/trip-planner2/test/integration Created TagLibTests for Date |
DateTagLib.groovy ファイルに、リスト 14 のコードを追加します。
リスト 14. カスタム
TagLib の作成
import java.text.SimpleDateFormat
class DateTagLib {
def customDateFormat = {attrs, body ->
def b = attrs.body ?: body()
def d = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(b)
//if no format attribute is supplied, use this
def pattern = attrs["format"] ?: "MM/dd/yyyy"
out << new SimpleDateFormat(pattern).format(d)
}
}
|
TagLib は属性という形での単純なString値、そしてタグの本体を取り、String を出力ストリームに送信します。このカスタム・タグを使用してフォーマットの指定されていない Date 型のフィールドをラップしようとしていることから、2 つの SimpleDateFormat オブジェクトが必要となります。まず、入力オブジェクトが Date.toString() 呼び出しのデフォルト・フォーマットと一致する String を読み込みます。これを適切な Date オブジェクトに構文解析すると、別のフォーマットの String として渡す 2 つ目の SimpleDateFormat オブジェクトを作成することができます。
list.gsp と show.gsp で、checkIn フィールドと checkOut フィールドを新しく作成した TagLib でラップします (リスト 15 を参照)。
リスト 15. カスタム
TagLib の使用
<g:customDateFormat>${fieldValue(bean:hotelStay, field:'checkIn')}</g:customDateFormat>
|
grails run-app と入力してから、http://localhost:9090/trip/hotelStay/list にアクセスし、カスタム TagLib が使用されていることを確認してください (図 13 を参照)。
図 13. カスタム
TagLib を使用した日付の出力
最後にリスト 16 に示すテストを作成して、TagLib が期待通りに機能することを確認します。
リスト 16. カスタム
TagLibのテスト
import java.text.SimpleDateFormat
class DateTagLibTests extends GroovyTestCase {
void testNoFormat() {
def output =
new DateTagLib().customDateFormat(format:null, body:"2008-10-01 00:00:00.0")
println "\ncustomDateFormat using the default format:"
println output
assertEquals "was the default format used?", "10/01/2008", output
}
void testCustomFormat() {
def output =
new DateTagLib().customDateFormat(format:"EEEE", body:"2008-10-01 00:00:00.0")
assertEquals "was the custom format used?", "Wednesday", output
}
}
|
いくつかのテストを用意してみて、Grails コンポーネントをテストするのがどんなに簡単かを実感してもらえたはずです。これからは、引き続きテスト駆動で開発を進めることによって、自分が行っている作業に一層確信が持てるようになります。また、ユーザー要件に合わせたテストを作成すれば、常に最新状態の充実した実行可能なドキュメント・セットを用意できるというメリットも得られます。
次回の記事では、JSON (JavaScript Object Notation) に焦点を当てます。Grails には極めて優れた JSON サポートが初めから備わっています。そこで、コントローラーから JSON を生成する方法、そして生成した JSON を GSP から使用する方法を紹介します。それまでは、Grails を楽しみながらマスターしてください。
学ぶために
- 連載「Grails をマスターする」: この連載の他の記事を読んで、Grails とこのフレームワークを使って実現可能なすべての内容をよく理解してください。
- Grails: Grails の Web サイトにアクセスしてください。
- Grails Framework Reference Documentation: Grails のバイブルとなります。
- 「Static Typing & Bureaucracy Redux」(Neil Ford、Meme Agora 共著、2007年5月): Neil Ford はこのブログ・エントリーで、動的言語を使うとより多くのテストをより短時間で実行できると言っています。
- Using JUnit 4 with Groovy: Groovy 1.5+ と Java 5+ を使用している限り、Grails で JUnit 4 を使用できます。ここに記載されているサンプル・コードを参考にしてください。
- Test'N'Groove: Test'N'Groove は、Groovy と TestNG の統合です。
- Unit Testing: Groovy ではどのようにして JUnit テストを容易に行えるようにするかを読んでください。サポートされるアサーションを網羅したリストも、ここに記載されています。
- 「コード品質を追求する: ビヘイビア駆動開発を舞台にした冒険」(Andrew Glover 著、developerWorks、2007年9月): プログラムの結果ではなく、その振る舞いに焦点を当てた開発方法について詳しく学んでください。
- 『Groovy Recipes』: Scott Davis の最新の著作で Groovy と Grails の詳細を学んでください。
- 連載「実用的な Groovy」: developerWorks のこの連載記事では、実用的な Groovy の使用方法を探り、それぞれの方法をいつ、どんな場合に適用するかを解説しています。
- Groovy:Groovy の Web サイトで、このプロジェクトの詳細を学んでください。
- AboutGroovy.com: Groovy に関する最新のニュースと記事へのリンクが記載されています。
- technology bookstore: この記事で紹介した技術やその他の技術に関する本を参照してください。
- developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
製品や技術を入手するために
議論するために
- developerWorks blogs から developerWorks コミュニティーに加わってください。
