【C#】GitHub Copilot Chatで値オブジェクトのxUnitテストを実装してもらう【初心者】

# はじめに
これまで自宅でVS Codeを使用してコーディングする際は、車輪の再発明によって言語に対する理解を深めたいという個人的理由から、GitHub Copilotを使うことは避けてきました。
が、実務でもGitHub Copilotを使う機会はなく、生成AIに関する勉強会に参加していると、今少しでもコーディングに生成AIを関与させる手段を覚えておかないと、時代に置いてけぼりにされる危機感が強くなったため、ものすごく今更ではありますが、体験記を記載します。

# 用意した値オブジェクト
数量を表す以下の値オブジェクトを実装しました。
今回はこのオブジェクトのxUnitテストをGitHub Copilot Chatに書いてもらおうと思います。

```c#:UnitOfAmount.cs
namespace WebApi.Domain.Value;

/// <summary>
/// 数量を表す値オブジェクト.
/// </summary>
public record UnitOfAmount
{
    /// <summary>
    /// 数量.
    /// </summary>
    public decimal Value { get; init; }

    /// <summary>
    /// コンストラクタ.
    /// </summary>
    /// <param name="value">値</param>
    /// <exception cref="ArgumentOutOfRangeException">0未満または1000を超える場合</exception>
    public UnitOfAmount(decimal value)
    {
        if (value < 0 || value > 1000)
        {
            throw new ArgumentOutOfRangeException(nameof(value));
        }
        Value = value;
    }

    public UnitOfAmount Add(decimal val)
    {
        return new(Value + val);
    }

    public UnitOfAmount Add(UnitOfAmount any)
    {
        return new(Value + any.Value);
    }

    public UnitOfAmount Minus(decimal val)
    {
        return new(Value - val);
    }

    public UnitOfAmount Minus(UnitOfAmount any)
    {
        return new(Value - any.Value);
    }

    public static UnitOfAmount operator +(UnitOfAmount a, UnitOfAmount b)
    {
        return a.Add(b);
    }

    public static UnitOfAmount operator -(UnitOfAmount a, UnitOfAmount b)
    {
        return a.Minus(b);
    }

    public static UnitOfAmount operator +(UnitOfAmount a, decimal b)
    {
        return a.Add(b);
    }

    public static UnitOfAmount operator -(UnitOfAmount a, decimal b)
    {
        return a.Minus(b);
    }

    public static bool operator >(UnitOfAmount a, UnitOfAmount b)
    {
        return a.Value > b.Value;
    }

    public static bool operator <(UnitOfAmount a, UnitOfAmount b)
    {
        return a.Value < b.Value;
    }

    public static bool operator >(UnitOfAmount a, decimal b)
    {
        return a.Value > b;
    }

    public static bool operator <(UnitOfAmount a, decimal b)
    {
        return a.Value < b;
    }

    public static bool operator >=(UnitOfAmount a, UnitOfAmount b)
    {
        return a.Value >= b.Value;
    }

    public static bool operator <=(UnitOfAmount a, UnitOfAmount b)
    {
        return a.Value <= b.Value;
    }

    public static bool operator >=(UnitOfAmount a, decimal b)
    {
        return a.Value >= b;
    }

    public static bool operator <=(UnitOfAmount a, decimal b)
    {
        return a.Value <= b;
    }
}
```
小難しいロジックのないオブジェクトで、あまりにもシンプルで面白くなかったので、演算子をいくつかオーバーライドしています。

# はじめてのGitHub Copilot Chatへの指示
新規ファイル「UnitOfAmountTest.cs」を作成し、GitHub Copilotには以下のように指示をしました。
```text:prompt
UnitOfAmount.csのxUnitTestを実装してください
```

## 出力結果
以下のコードが自動生成されました。
```c#:UnitOfAmountTest.cs
using System;
using WebApi.Domain.Value;
using Xunit;

namespace WebApi.Tests.Domain.Value.UnitOfAmountTest
{
    public class UnitOfAmountTest
    {
        [Fact]
        public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueIsLessThanZero()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => new UnitOfAmount(-1));
        }

        [Fact]
        public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueIsGreaterThanThousand()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => new UnitOfAmount(1001));
        }

        [Fact]
        public void Constructor_ShouldSetValue_WhenValueIsWithinRange()
        {
            var unit = new UnitOfAmount(500);
            Assert.Equal(500, unit.Value);
        }

        [Fact]
        public void Add_ShouldReturnNewUnitOfAmount_WithAddedValue()
        {
            var unit = new UnitOfAmount(500);
            var result = unit.Add(100);
            Assert.Equal(600, result.Value);
        }

        [Fact]
        public void Add_ShouldReturnNewUnitOfAmount_WithAddedUnitOfAmountValue()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(100);
            var result = unit1.Add(unit2);
            Assert.Equal(600, result.Value);
        }

        [Fact]
        public void Minus_ShouldReturnNewUnitOfAmount_WithSubtractedValue()
        {
            var unit = new UnitOfAmount(500);
            var result = unit.Minus(100);
            Assert.Equal(400, result.Value);
        }

        [Fact]
        public void Minus_ShouldReturnNewUnitOfAmount_WithSubtractedUnitOfAmountValue()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(100);
            var result = unit1.Minus(unit2);
            Assert.Equal(400, result.Value);
        }

        [Fact]
        public void OperatorPlus_ShouldReturnNewUnitOfAmount_WithAddedValues()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(100);
            var result = unit1 + unit2;
            Assert.Equal(600, result.Value);
        }

        [Fact]
        public void OperatorMinus_ShouldReturnNewUnitOfAmount_WithSubtractedValues()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(100);
            var result = unit1 - unit2;
            Assert.Equal(400, result.Value);
        }

        [Fact]
        public void OperatorGreaterThan_ShouldReturnTrue_WhenFirstValueIsGreater()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(100);
            Assert.True(unit1 > unit2);
        }

        [Fact]
        public void OperatorLessThan_ShouldReturnTrue_WhenFirstValueIsLess()
        {
            var unit1 = new UnitOfAmount(100);
            var unit2 = new UnitOfAmount(500);
            Assert.True(unit1 < unit2);
        }

        [Fact]
        public void OperatorGreaterThanOrEqual_ShouldReturnTrue_WhenFirstValueIsGreaterOrEqual()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(500);
            Assert.True(unit1 >= unit2);
        }

        [Fact]
        public void OperatorLessThanOrEqual_ShouldReturnTrue_WhenFirstValueIsLessOrEqual()
        {
            var unit1 = new UnitOfAmount(100);
            var unit2 = new UnitOfAmount(500);
            Assert.True(unit1 <= unit2);
        }
    }
}
```

一通りのメソッドに対するテストは実装してくれましたが、境界テストまでは作り込んでくれませんでした。

# 指示を詳細化してみる
今度は指示に少し具体性を追加します。
```text:prompt
UnitOfAmount.csのxUnitTestを実装してください
境界値と同値クラステストを行なってください
メソッド名は日本語にしてください
```

## 出力結果

```c#:UnitOfAmountTest.cs
using System;
using WebApi.Domain.Value;
using Xunit;

namespace WebApi.Tests.Domain.Value.UnitOfAmountTest
{
    public class UnitOfAmountTest
    {
        [Fact]
        public void コンストラクタ_正常値()
        {
            var unit = new UnitOfAmount(500);
            Assert.Equal(500, unit.Value);
        }

        [Fact]
        public void コンストラクタ_境界値_0()
        {
            var unit = new UnitOfAmount(0);
            Assert.Equal(0, unit.Value);
        }

        [Fact]
        public void コンストラクタ_境界値_1000()
        {
            var unit = new UnitOfAmount(1000);
            Assert.Equal(1000, unit.Value);
        }

        [Fact]
        public void コンストラクタ_異常値_マイナス()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => new UnitOfAmount(-1));
        }

        [Fact]
        public void コンストラクタ_異常値_1001()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => new UnitOfAmount(1001));
        }

        [Fact]
        public void 加算_正常値()
        {
            var unit = new UnitOfAmount(500);
            var result = unit.Add(200);
            Assert.Equal(700, result.Value);
        }

        [Fact]
        public void 減算_正常値()
        {
            var unit = new UnitOfAmount(500);
            var result = unit.Minus(200);
            Assert.Equal(300, result.Value);
        }

        [Fact]
        public void 加算_異常値_境界超え()
        {
            var unit = new UnitOfAmount(900);
            Assert.Throws<ArgumentOutOfRangeException>(() => unit.Add(200));
        }

        [Fact]
        public void 減算_異常値_境界超え()
        {
            var unit = new UnitOfAmount(100);
            Assert.Throws<ArgumentOutOfRangeException>(() => unit.Minus(200));
        }

        [Fact]
        public void 比較_大なり()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(300);
            Assert.True(unit1 > unit2);
        }

        [Fact]
        public void 比較_小なり()
        {
            var unit1 = new UnitOfAmount(200);
            var unit2 = new UnitOfAmount(300);
            Assert.True(unit1 < unit2);
        }

        [Fact]
        public void 比較_等しい()
        {
            var unit1 = new UnitOfAmount(300);
            var unit2 = new UnitOfAmount(300);
            Assert.True(unit1 == unit2);
        }
    }
}
```

いい感じになりましたが、今度はオーバーロードしているメソッドのテストが足りません。

# もう少し指示を追加
さらに一言添えてテストを書いてもらいます。
```text:prompt
UnitOfAmount.csのxUnitTestを実装してください
境界値と同値クラステストを行なってください
メソッド名は日本語にしてください
オーバーロードしているメソッドもテストしてください
オーバーライドしているオペラントのテストもしてください
booleanを返すテストは結果がFalseのパターンもテストしてください
```

## 出力結果
だいぶいい感じになりました。が、やっぱりちょっと足りません。
```c#:UnitOfAmountTest.cs
using System;
using WebApi.Domain.Value;
using Xunit;

namespace WebApi.Tests.Domain.Value.UnitOfAmountTest
{
    public class UnitOfAmountTest
    {
        [Fact]
        public void コンストラクタ_正常値()
        {
            var unit = new UnitOfAmount(500);
            Assert.Equal(500, unit.Value);
        }

        [Fact]
        public void コンストラクタ_境界値_0()
        {
            var unit = new UnitOfAmount(0);
            Assert.Equal(0, unit.Value);
        }

        [Fact]
        public void コンストラクタ_境界値_1000()
        {
            var unit = new UnitOfAmount(1000);
            Assert.Equal(1000, unit.Value);
        }

        [Fact]
        public void コンストラクタ_異常値_マイナス()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => new UnitOfAmount(-1));
        }

        [Fact]
        public void コンストラクタ_異常値_1001()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => new UnitOfAmount(1001));
        }

        [Fact]
        public void 加算メソッド_正常値()
        {
            var unit = new UnitOfAmount(500);
            var result = unit.Add(200);
            Assert.Equal(700, result.Value);
        }

        [Fact]
        public void 加算メソッド_オーバーロード_正常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(200);
            var result = unit1.Add(unit2);
            Assert.Equal(700, result.Value);
        }

        [Fact]
        public void 減算メソッド_正常値()
        {
            var unit = new UnitOfAmount(500);
            var result = unit.Minus(200);
            Assert.Equal(300, result.Value);
        }

        [Fact]
        public void 減算メソッド_オーバーロード_正常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(200);
            var result = unit1.Minus(unit2);
            Assert.Equal(300, result.Value);
        }

        [Fact]
        public void 加算オペラント_正常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(200);
            var result = unit1 + unit2;
            Assert.Equal(700, result.Value);
        }

        [Fact]
        public void 減算オペラント_正常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(200);
            var result = unit1 - unit2;
            Assert.Equal(300, result.Value);
        }

        [Fact]
        public void 比較演算子_大なり_正常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(200);
            Assert.True(unit1 > unit2);
        }

        [Fact]
        public void 比較演算子_大なり_異常値()
        {
            var unit1 = new UnitOfAmount(200);
            var unit2 = new UnitOfAmount(500);
            Assert.False(unit1 > unit2);
        }

        [Fact]
        public void 比較演算子_小なり_正常値()
        {
            var unit1 = new UnitOfAmount(200);
            var unit2 = new UnitOfAmount(500);
            Assert.True(unit1 < unit2);
        }

        [Fact]
        public void 比較演算子_小なり_異常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(200);
            Assert.False(unit1 < unit2);
        }

        [Fact]
        public void 比較演算子_大なりイコール_正常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(500);
            Assert.True(unit1 >= unit2);
        }

        [Fact]
        public void 比較演算子_大なりイコール_異常値()
        {
            var unit1 = new UnitOfAmount(200);
            var unit2 = new UnitOfAmount(500);
            Assert.False(unit1 >= unit2);
        }

        [Fact]
        public void 比較演算子_小なりイコール_正常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(500);
            Assert.True(unit1 <= unit2);
        }

        [Fact]
        public void 比較演算子_小なりイコール_異常値()
        {
            var unit1 = new UnitOfAmount(500);
            var unit2 = new UnitOfAmount(200);
            Assert.False(unit1 <= unit2);
        }
    }
}
```

細かいところまで生成AIに漏れなく作ってもらうには更に工夫が必要そうですが、そこに時間をかけるくらいなら、あとは人の手で不足ケースをコピペで足していけば良いかと思います。
100行超のテストを数秒で書いてくれるので、それだけでも開発体験が著しく向上しました。

# 使ってみることが大事
今回実際にテストを自動生成したことで、ざっくりとした質問だけでは十分なテストを作ってもらえないこと、具体的な指示を出さなければならないことが理解できました。
GitHub Copilot Chatへどのような指示を出せば、より求めているテストを書いてくれるようになるかは、実際に指示を出しながらコツを掴んでいく必要がありそうです。
そう言った意味でも、まだGitHub Copilotに触れていない人は、今からでも遅くないので利用してみることをお勧めします。