🐅
「コンパイル時のユニットテスト」導入するとユニットテストを書かなくてよくなるのか?というタイトルで登壇しました
株式会社ジェイテックジャパン CTOの高丘 @tomohisaです。2024年3月22日、Nextbeat Tech Bar:第一回ソフトウェアテストについて考える会で登壇しました!
t-wada さんの後の登壇とのことで緊張しましたが、なんとか無事に行うことができました。
スライドはこちらになります。
登壇に先立って一人Zoomで行った練習をYoutubeに更新しましたのでよろしければご覧ください。
簡単なまとめを以下に記載します。
コンパイル時のユニットテストとはなんなのか?
- 「コンパイル時のユニットテスト」とは Scott Wlaschin 氏のDomain Modeling Made Functional に関する登壇で出てきた表現
- 1つのオブジェクトの状態の遷移をパラメータやフラグで表現するのではなく、別の型として定義する。各機能は特定のオブジェクトの状態でないと実行できないように動作を制限する。
- 不正なデータを入れることができず、入れようとするとコンパイルエラーとなるため、「コンパイル時のユニットテスト」と表現している
- その部分に関してはコンパイルでチェックできるので”テストを書く必要がない”と書いており、ロジックのテストが不要であると言っているわけではない。
「コンパイル時のユニットテスト」どのように記述するのか?
① フラグを使ったオブジェクト指向的な書き方
type EmailAddress = EmailAddress of string
type EmailContactInfo = {
EmailAddress: EmailAddress
IsEmailVerified: bool
}
② 状態ごとに型を変える書き方
type EmailAddress = EmailAddress of string
type VerifiedEmail = VerifiedEmail of EmailAddress
type UnverifiedEmail = UnverifiedEmail of EmailAddress
type EmailContactInfo = VerifiedEmail | UnverifiedEmail
① フラグを使ったオブジェクト指向的な書き方 C#
public record Email([property:EmailAddress]string Value);
public record EmailContactInfo(Email Email, bool IsVerified);
public record Customer(string Name, EmailContactInfo ContactInfo);
② 状態ごとに型を変える書き方 C#
public record Email([property:EmailAddress]string Value);
public interface IEmailContactInfo;
public record VerifiedEmailAddress(Email Email) : IEmailContactInfo;
public record UnverifiedEmailAddress(Email Email) : IEmailContactInfo;
public record Customer(string Name, IEmailContactInfo ContactInfo);
パスワードリセットメール送信
public static class EmailService2
{
public static async Task<IEmailContactInfo> ValidateEmailAsync(Email email)
{
// 何か長い処理で検証する。正しいメールアドレスというだけでなく
// 送信しても大丈夫かなども検証する。
await Task.CompletedTask;
var isValid = true;
return isValid ?
new VerifiedEmailAddress(email)
: new UnverifiedEmailAddress(email);
}
public static bool ResetEmail(VerifiedEmailAddress email, string name)
{
// リセットメールを送る failed@example.com の時だけ失敗(仮)
var result = !email.Email.Value.Equals("failed@example.com");
return result;
}
}
「コンパイル時のユニットテスト」テストにどんな影響があるのか
① フラグを使ったオブジェクト指向的な書き方 C#
public class TestDomain
{
[Fact]
public void TestShouldFailWhenIsVerifiedFalse()
{
var email = new EmailContactInfo(new Email("test@example.com"), false);
var result = EmailService.ResetEmail(email, "John Doe");
Assert.False(result);
}
[Fact]
public void TestShouldSuccessWhenIsVerifiedTrue()
{
var email = new EmailContactInfo(new Email("test@example.com"), true);
var result = EmailService.ResetEmail(email, "John Doe");
Assert.True(result);
}
}
② 状態ごとに型を変える書き方 C#
public class TestStaticTypedDomain
{
[Fact]
public void TestShouldSuccessWhenIsVerifiedTrue()
{
var email = new VerifiedEmailAddress(new Email("test@example.com"));
var result = EmailService2.ResetEmail(email, "John Doe");
Assert.True(result);
}
// 不要になるテスト
// [Fact]
// public void TestShouldSuccessWhenIsVerifiedFalse()
// {
// var email = new UnverifiedEmailAddress(new Email("test@example.com"));
// // コンパイルでUnverifiedEmailAddressを渡すことができないので、このテスト自体
// // は書けないし、書く必要がない
// var result = EmailService2.ResetEmail(email, "John Doe");
// Assert.True(result);
// }
}
静的な型を使ってモデリングをした場合...
- 特定の状態の型に対して機能を記述できるため、フラグのチェックが不要になる。正しいデータしか入力できないため入力に関するテストが不要になる。
- 全体としてそれぞれの状態でどう振る舞うかに関してのテストは必要であり、テストの数は減るが無くなるわけではない
- フラグを持たないことによる型の自己文書化 ↔️ 型が増えることによって管理が面倒になるというトレードオフがある
注意点
パターンマッチングと判別共用体に関して
永続化と復元に関して
静的な型の合成を使ってのモデリングを行ってみての感想
- 静的な型によるデータの変換にフォーカスすることにより、複雑なプログラムを記述しやすくなった
- 型によるパターンマッチやswitch式を多用するようになり、if 文で記述することが少なくなった。
- フラグをできるだけ無くして型の遷移でビジネスロジックを表現することにより、型の数は多いが、それぞれの型とロジックはシンプルに表現できるようになった
イベントソーシング
- Domain Modeling Made Functionalの本は基本的に関数型でドメインモデリングを行い、イベントソーシングで実装する方法について書かれている。
- 関数型ドメインモデリングはイベントソーシングと相性はよく、データの状態の遷移をイベントで表現して、ドキュメントデータベースにデータを保存することにより扱いやすくなる。
- ただ、関数型ドメインモデリングはイベントソーシングを採用しなくても実践可能
- 私たちの開発している Sekiban は簡単にイベントソーシングと型の合成を使ったモデリングを行うのに適している
まとめ
今回のイベントはオンライン、オフライン含めて100人以上の参加でとても盛況でした。
いろんな質問やコメントもXやmeetのチャットで見ることができ、とても楽しかったです。
Xでの反応を一部貼っておきます!
Discussion