🐅

「コンパイル時のユニットテスト」導入するとユニットテストを書かなくてよくなるのか?というタイトルで登壇しました

2024/03/23に公開

株式会社ジェイテックジャパン CTOの高丘 @tomohisaです。2024年3月22日、Nextbeat Tech Bar:第一回ソフトウェアテストについて考える会で登壇しました!

https://nextbeat.connpass.com/event/309287/

t-wada さんの後の登壇とのことで緊張しましたが、なんとか無事に行うことができました。
スライドはこちらになります。

https://speakerdeck.com/tomohisa/konpairushi-noyunitutotesuto-dao-ru-surutoyunitutotesutowo-shu-kanakuteyokunarunoka

登壇に先立って一人Zoomで行った練習をYoutubeに更新しましたのでよろしければご覧ください。

https://www.youtube.com/watch?v=6CAos7CCAnE

簡単なまとめを以下に記載します。

コンパイル時のユニットテストとはなんなのか?

  • 「コンパイル時のユニットテスト」とは Scott Wlaschin 氏のDomain Modeling Made Functional に関する登壇で出てきた表現
  • 1つのオブジェクトの状態の遷移をパラメータやフラグで表現するのではなく、別の型として定義する。各機能は特定のオブジェクトの状態でないと実行できないように動作を制限する。
  • 不正なデータを入れることができず、入れようとするとコンパイルエラーとなるため、「コンパイル時のユニットテスト」と表現している
  • その部分に関してはコンパイルでチェックできるので”テストを書く必要がない”と書いており、ロジックのテストが不要であると言っているわけではない。

slide1

「コンパイル時のユニットテスト」どのように記述するのか?

① フラグを使ったオブジェクト指向的な書き方

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);

パスワードリセットメール送信

slide2

slide3

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);
    // }
}

静的な型を使ってモデリングをした場合...

  • 特定の状態の型に対して機能を記述できるため、フラグのチェックが不要になる。正しいデータしか入力できないため入力に関するテストが不要になる。
  • 全体としてそれぞれの状態でどう振る舞うかに関してのテストは必要であり、テストの数は減るが無くなるわけではない
  • フラグを持たないことによる型の自己文書化 ↔️ 型が増えることによって管理が面倒になるというトレードオフがある

注意点

パターンマッチングと判別共用体に関して
slide3

永続化と復元に関して
slide3

静的な型の合成を使ってのモデリングを行ってみての感想

  • 静的な型によるデータの変換にフォーカスすることにより、複雑なプログラムを記述しやすくなった
  • 型によるパターンマッチやswitch式を多用するようになり、if 文で記述することが少なくなった。
  • フラグをできるだけ無くして型の遷移でビジネスロジックを表現することにより、型の数は多いが、それぞれの型とロジックはシンプルに表現できるようになった

イベントソーシング

  • Domain Modeling Made Functionalの本は基本的に関数型でドメインモデリングを行い、イベントソーシングで実装する方法について書かれている。
  • 関数型ドメインモデリングはイベントソーシングと相性はよく、データの状態の遷移をイベントで表現して、ドキュメントデータベースにデータを保存することにより扱いやすくなる。
  • ただ、関数型ドメインモデリングはイベントソーシングを採用しなくても実践可能
  • 私たちの開発している Sekiban は簡単にイベントソーシングと型の合成を使ったモデリングを行うのに適している

まとめ

今回のイベントはオンライン、オフライン含めて100人以上の参加でとても盛況でした。

いろんな質問やコメントもXやmeetのチャットで見ることができ、とても楽しかったです。

Xでの反応を一部貼っておきます!

https://twitter.com/uechang16/status/1771125844402176303

https://twitter.com/ratmie1/status/1771125668337873255

https://twitter.com/keita44_f4/status/1771125570954494194

https://twitter.com/keita44_f4/status/1771125162970427407

https://twitter.com/keita44_f4/status/1771124686665175215

https://twitter.com/yoshi_engineer_/status/1771124245965541720

https://twitter.com/ratmie1/status/1771124081364279465

https://twitter.com/kmizu/status/1771124056345219280

https://twitter.com/naghbIQtIqHom/status/1771123977404256616

https://twitter.com/uechang16/status/1771123934223815073

https://twitter.com/katzkb/status/1771123875184787925

https://twitter.com/verdy_266/status/1771123749552865510

https://twitter.com/keita44_f4/status/1771123725209051408

https://twitter.com/kmizu/status/1771123663699685660

https://twitter.com/tomoh1r/status/1771123606417981675

https://twitter.com/ratmie1/status/1771123585391927546

https://twitter.com/boykush315/status/1771123501245890965

https://twitter.com/ratmie1/status/1771123299101352430

https://twitter.com/naghbIQtIqHom/status/1771122742055825601

ジェイテックジャパンブログ

Discussion