【CSM】認定スクラムマスター研修を受講してきた
会社から認定スクラムマスター研修に参加してみないかと声をかけていただき、二日間の研修を受講し認定スクラムマスターの資格を取得したのでその感想です。
調査
まずは研修について調べるところから始まりました。
いくつか主催しているところを見てまわりましたが、スケジュールと価格で私が選んだのはこちら。 www.abi-agile.com
予約申し込みはPeatixというプラットフォームから行います。登録してしばらくすると主催からメールに連絡があります。内容はZoomの接続情報、Miroの使い方、スクラムガイドなどの参考資料です。 当日までに、使ったことがなければZoomやMiroの基本操作の練習をして一通り参考資料に目を通しておくと良いでしょう。研修中にZoomで画面共有する機会があったのですが、私は事前の設定が足りなかったため他の人にお願いすることになりました…。Miroは画面の移動やポストイットの貼り付けなど簡単な操作について試しておくと安心です。
研修
研修は二日間の日程で、10時から16時過ぎまでのZoomとMiroを使ったオンライン研修となります。
開始5分前にZoomに接続確認する時間があります。余裕を持って接続しておきましょう。基本的にカメラは常時オンでした。私は普段リモートでカメラは切っているので初日はこのせいで疲れました💦
参加者は20名程いました。
研修内容はHPに記載されている下記内容について、3~4人で1グループとなりプロダクトオーナー(PO)、スクラムマスター(SM)、開発者(Developer)の役割をローテーションしながら進んでいきます。
<クラス学習内容>
・スクラムの本質とその主な原則
・スクラム3つの役割と責任
・スクラム5つのイベントとは
・スクラム3つの作成物の作り方・管理方法
・バックログアイテムの見積もり・納期の予測
・スプリントの実践
・継続的な改善とチームワーク
1日目は16時終了となりますが、その後に20分程度自由に質問できる時間があります。参加した日が平日ということもあり、ほとんどの方が仕事に戻られたようで4人しか残らなかったため、いくつか質問できました。
下記はそのうちの一つです。
Question. 複数回のスプリントで分割して並行にプロダクトを作成する場合に、終盤で組み上げた時でないと動作する価値あるインクリメントにならないのではないか?
Answer. 完成したものでなくても、価値のあるものから作ることでリスクを減らすことはできる。例えばロケットで言えばエンジンから。
よくイメージできないことだったり、こんな時どうする?みたいな質問を事前に用意しておくと研修の効果が高まります。初歩的なことなので、他の参加者の前で質問しにくかったりする場合はMiroのポストイットに書いておくとテキストで返信もくれます👍
研修で紹介されていた動画も面白かったので貼っておきます。
スクラムチームが店頭で顧客のフィードバックを直接得ながら開発する様子。ハイパフォーマンスなチームで仕事するってこんな感じなのか。
PO視点でのアジャイル開発について。アジャイル開発をしていたけどバックログが溜まりすぎてウォータフォールに戻したというプロジェクトの話を聞いたことがありますが、何が機能していなかったのか(POの一番重要な仕事)について明確に語られています。
認定試験
受講終了後(私は2時間後くらい)Scrum AllianceからWelcomeメールが届きます。このメールのリンクからアカウントを作成し、受験することが可能となります。 アカウントの作成は30日以内、受験は90日以内であれば可能で2回分の受験料が含まれているとのことだったので私はその日の内に試験を受けてしまいました。
試験自体は特別難しくはなく、アジャイルソフトウェア開発技術者検定Lv.1よりも簡単だと感じました。 ちなみに全問正解だと思ったのですが、4問間違えました😉
雑感
私自身はアジャイル開発の経験はなく、ウォータフォール開発の現場で仕事をしています。ただ、アジャイル開発には興味があったので、本を読み、イベントなどに足を運んでいました。また、会社でもアジャイル開発をやってみたいと上司に言っていたので今回認定スクラムマスター研修に行ってみないかと声がかかりました。行動と発信は大事!
アジャイル開発/スクラムマスターの経験がなかったので研修を受けるにあたり不安はありました。実際に参加者はスクラム経験のある方が多い印象でしたが、CSMはスクラムマスターの基礎知識を有していることの証明なので、研修は未経験でも問題はないと感じました。もちろん、ある程度は実務経験があった方が有効な質問ができるなど研修の効果は高いと思います。
他の主催者さんの研修を受講していないため比較はできませんが、研修は終始楽しい雰囲気で進められました。チームメンバーも積極的に意見、行動を起こす方ばかりだったのでメンバーから学ぶことも多かったです。
講師は基本英語ですが、常に翻訳者の方が訳してくれるので理解できないということはありません。どうしても翻訳の時間があるので英語が理解できたら、もしくは、日本語で研修があればより多く学ぶ時間があるなと考えてしまいました。
スクラムマスターの役割の一つに、チームや組織の変革の担い手という責任があります。認定スクラムマスターの資格を取得して次のアクションは、社内勉強会で研修で学んだことの共有など組織に対してスクラムの導入や啓蒙を支援することが直近の私の役目かなと思っています。
Java テストコードにおける複雑なオブジェクトのセットアップ
概要
テストコードを書くときに入力値や期待値として複雑なオブジェクトのセットアップをしなければならない、稀によくありますよね。現在参画中のプロジェクトではテストクラス内に大量にセットアップ用コードがあり、開発と共に増え続けていきます。可読性も悪く、メンテナンスも辛い。それじゃ、外部ファイルから読み込めばいいんじゃないというお話です。
環境
複雑なオブジェクトのセットアップが辛い
現状のテストコードは生成メソッドを大量に書くか、制御構文である程度まとめています。テストクラス内に書いているため、ほとんどがセットアップ用のコードで埋め尽くされているような状態です。これをなんとかしたい。
テストケースごとに生成メソッドを用意
シンプルですが大量にメソッドが作成されます。複数のケースで同じデータを扱っていても共通化はしていません。共通データを修正する際は複数箇所を書き直すのが辛いですがテストケースごとに独立はしています。
@Test public void test() { // 期待値オブジェクトの生成 var expected = expected_01(); // 検証 ... } private ComplexData expected_01() { // 複雑なオブジェクトの生成 return ComplexData.builder() // 実際はここからさらにセットアップメソッドを呼ぶ ... .build(); }
テストケースごとにifやSwitch構文で共通のデータをまとめる
共通のデータをまとめます。修正時は直す箇所が少ないのがメリットですが、共通ではなくなった時にメンテナンスが辛いのと、可読性は落ちます。
@Test public void test() { int caseNo = 2; // 期待値オブジェクトの生成 var expected = expected(caseNo); // 検証 ... } private ComplexData expected(int caseNo) { switch (caseNo) { case 1: // 複雑なオブジェクトの生成 return ComplexData.builder() // 実際はここからさらにセットアップメソッドを呼ぶ ... .build(); case 2: // 複雑なオブジェクトの生成 return ComplexData.builder() // 実際はここからさらにセットアップメソッドを呼ぶ ... .build(); default: return ComplexData.builder() // 実際はここからさらにセットアップメソッドを呼ぶ ... .build(); } }
外部ファイルからオブジェクトの生成を行う
どうしたら良いのかとネットや本で調べていたら下記書籍に書いてありました。
外部ファイルに定義したリソースファイルにテストデータを記述し、生成メソッドなどで読み込む手法が有効です。
SnakeYamlというライブラリを使用してYAMLファイルからセットアップする方法が記載されていたので、この方法が良さそうです。
シンプルなオブジェクト
まずはシンプルなオブジェクトの生成から試してみました。
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class Task { private String id; private int number; private String name; private LocalDateTime start; private LocalDateTime end; private LocalDate expire; private boolean completed; }
YAMLファイルはこんな感じ。
id: "1" number: 101 name: "Homework" start: "2021-08-20 16:30:00" end: null expire: "2021-08-22" completed: false
テストコードはYAMLファイルからオブジェクトを生成できるか確認したいので、ここでは期待値はインラインで作成しています。
@Test public void test() throws IOException { Task expected = Task.builder() .id("1") .number(101) .name("Homework") .start(LocalDateTime.of(2021, 8, 20, 16, 30, 0)) .end(null) .expire(LocalDate.of(2021, 8, 22)) .completed(false) .build(); var path = Paths.get("path/to/task.yml"); try (InputStream io = Files.newInputStream(path)) { Constructor constructor = new Constructor(Task.class); Yaml yaml = new Yaml(constructor); Task actual = yaml.load(io); assertThat(actual).usingRecursiveComparison().isEqualTo(expected); } }
残念ながらLocalDate
とLocalDateTime
型は非対応っぽいので、そのままでは動きませんでした。
Constructor
というクラスを拡張して、定義したタグが付いていたらLocalDate
やLocalDateTime
にパースしてあげるって感じにしてあげたらいいっぽい。
import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import org.yaml.snakeyaml.constructor.Constructor; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.Tag; public class ExtensionConstructorBase extends Constructor { private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; public ExtensionConstructorBase() { this(Object.class); } public ExtensionConstructorBase(Class<? extends Object> theRoot) { super(theRoot); setup(); } private void setup() { yamlConstructors.put(new Tag("!localdate"), new LocalDateConstructor()); yamlConstructors.put(new Tag("!localdatetime"), new LocalDateTimeConstructor()); } protected class LocalDateConstructor extends Constructor.ConstructScalar { @Override public Object construct(Node node) { return LocalDate.parse(((ScalarNode) node).getValue()); } } protected class LocalDateTimeConstructor extends Constructor.ConstructScalar { @Override public Object construct(Node node) { DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DATETIME_FORMAT); return LocalDateTime.parse(((ScalarNode) node).getValue(), dtf); } } }
YAMLファイルにはそれぞれ定義したタグをつける。
id: "1" number: 101 name: "Homework" start: !localdatetime "2021-08-20 16:30:00" end: null expire: !localdate "2021-08-22" completed: false
テストコードはConstructor
を新しく作成したExtensionConstructorBase
に差し替え。
Constructor constructor = new ExtensionConstructorBase(Task.class);
複雑なオブジェクト
次に現実でありそうな少し複雑なオブジェクトです。customers
プロパティにはCustomer
クラスのコレクションが格納されます。対してitems
プロパティにはItem
クラスをスーパークラスとしたサブクラス(Glasses
やBag
など)のコレクションが格納されます。
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class ComplexData { private String id; private Address address; private List<Customer> customers; private List<Item> items; }
サブクラスのコレクションについては、YAMLファイルの定義からサブクラスを紐付ける必要があるため、ExtensionConstructorBase
をさらに拡張してタグを用意し、各クラスに対応するよう定義しました。
import org.yaml.snakeyaml.TypeDescription; import org.yaml.snakeyaml.nodes.Tag; public class ComplexDataConstructor extends ExtensionConstructorBase { public ComplexDataConstructor() { super(ComplexData.class); this.addTypeDescription(new TypeDescription(Glasses.class, new Tag("!glasses"))); this.addTypeDescription(new TypeDescription(Bag.class, new Tag("!bag"))); } }
YAMLファイルはこんな感じ。
id: 1 address: id: 1 zip: 1234567 street: Fuji St. city: Tokyo country: JPN customers: - { customerId: 1, customerName: customer1 } - { customerId: 2, customerName: customer2 } items: - !glasses { id: 1, lens: blue } - !bag { id: 2, capacity: L }
ついでにYAMLを読み込む部分はクラスに抽出しました。引数なしコンストラクタでExtensionConstructorBase
を指定することでLocalDate
など共通の設定が反映されるようにしています。
public class YamlLoader<T> { private Constructor constructor; private static final String BASE_PATH = "src/test/resources/"; public YamlLoader() { this.constructor = new ExtensionConstructorBase(); } public YamlLoader(Constructor constructor) { this.constructor = constructor; } public T load(String yml) throws IOException { var path = Paths.get(BASE_PATH + yml); try (InputStream io = Files.newInputStream(path)) { return new Yaml(this.constructor).load(io); } } }
テストコードはYAMLファイルの読み込みや設定を別クラスに切り出したことで少しシンプルになったかなと思います。
private YamlLoader<ComplexData> yamlLoader; @BeforeEach void setup() { yamlLoader = new YamlLoader<>(new ComplexDataConstructor()); } @Test public void test() throws IOException { var yml = "path/to/complexdata.yml"; var expected = yamlLoader.load(yml); var actual = target.findById(id); assertThat(actual).usingRecursiveComparison().isEqualTo(expected); }
外部ファイルから複雑なオブジェクトのセットアップをすることでテストコードの肥大化を防ぐことができそうです。今回のサンプルでは設定周りのコードが必要なので辛い部分は残ります。また、テストケースと同じだけの大量の外部ファイルが作られるという別の課題も出て来そうです。いずれにせよ改善の余地はまだあるということでしょう。
完全なコードはGitHubにあげています。