こんにちは、サイオステクノロジーの佐藤 陽です。
今月はSIOS Technologyのアドベントカレンダー月間であり、テーマは「生成AI」です。
いろんなメンバーが生成AI活用に関する記事を投稿していくので、楽しみにしててください。
1日目の今日は、わたくし佐藤が「GitHub Copilotと一緒にTDDしてみた」と題した記事を書いていきたいと思います。
はじめに
本記事は以下の記事にインスパイアされた記事になります。
内容としてはほぼ同じで、それを.NET環境で実際に試してみました。
テストのフレームワークとしてはxUnitを利用します。
Abstract
時間がない方向けに結論を先に書いておきます。
- TDDのアプローチをとることで、Copilotの提案する内容の質が向上することは確認できました。
- GitHubCopilotから質の高い提案を受けるためには、こちらから様々な情報を与える必要があるように感じました。
- TDDの細かいサイクル(TODOリストを書く→テストを書く→失敗する→実装を行う→成功する→リファクタリング and more)でヒントを与えながら実装をすることで、Copilotの提案の質が向上することを実感しました。
実装
環境構築
まず.NETの環境を構築します。 VisualStudio上でポチポチやってもらってもOKですし、以下のようなコマンドを叩いていただいてもOKです。
dotnet new sln -o ai-tdd-dotnet
cd ai-tdd-dotnet
dotnet new classlib -o FizzBuzzTdd
mv ./FizzBuzzTdd/Class1.cs ./FizzBuzzTdd/FizzBuzzTdd.cs
dotnet sln add ./FizzBuzzTdd/FizzBuzzTdd.csproj
dotnet new xunit -o FizzBuzzTdd.Tests
dotnet add ./FizzBuzzTdd.Tests/FizzBuzzTdd.Tests.csproj reference ./FizzBuzzTdd/FizzBuzzTdd.csproj
dotnet sln add ./FizzBuzzTdd.Tests/FizzBuzzTdd.Tests.csproj
アプリ本体の方は以下のような形でクラスが構成されます。
namespace FizzBuzzTdd;
public class Class1
{
}
テストプロジェクトの方は以下のような形です。
namespace FizzBuzzTdd.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
TDDフロー
では早速テスト書いていきましょう。
まずはテストの部分に早速TodoListを追記します。
using FizzBuzz;
namespace FizzBuzz.Tests
{
public class UnitTest1
{
[Fact]
public void Test1()
{
}
/**
* TODO List
* - [] 3の倍数の場合は"Fizz"を返す
*/
}
}
1つTODO Listを記載すると、以下のように他の内容も提案されました。
public class UnitTest1
{
[Fact]
public void Test1()
{
}
/**
* TODO List
* - [] 3の倍数の場合は"Fizz"を返す
* - [] 5の倍数の場合は"Buzz"を返す
* - [] 3と5の倍数の場合は"FizzBuzz"を返す
* - [] それ以外の場合はそのまま返す
*/
}
では、TDDを始めていきます。
まず「3の倍数の場合は”Fizz”を返す」のケースのテストを書いていきます。
既存のUnitTest1に以下の説明文を追加して、Copilotから提案を受けます。
[Fact(DisplayName = "3の倍数の場合はFizzを返す")]
public void Test1()
{
}
最初に提案してきたテスト内容は以下の通りです。
[Fact(DisplayName = "3の倍数の場合はFizzを返す")]
public void Test1()
{
//Arrange
var fizzBuzz = new FizzBuzz.FizzBuzz();
//Act
var result = fizzBuzz.FizzBuzzFunction(1);
//Assert
Assert.Equal("1", result);
}
インスタンスの生成方法も誤っているし、Assertの部分も異なっています。
これを一度正しく修正します。
最初のテストが今後の学習内容にもなるため、丁寧に書き直します。
[Fact(DisplayName = "3の倍数の場合はFizzを返す")]
public void Test1()
{
//Arrange
var fizzBuzz = new FizzBuzz();
//Act
var result = fizzBuzz.FizzBuzzFunction(3);
//Assert
Assert.Equal("Fizz", result);
}
これを実行すると、テストは失敗します。
TDDの流れとしては想定通りです。
ではこのテストが通るように本体のアプリを書き直します。
クラス名を修正し、提案を受けると以下のような形で実装されました。
namespace FizzBuzzTdd;
public class FizzBuzz
{
public string FizzBuzzFunction(int number)
{
if (number % 3 == 0)
{
return "Fizz";
}
return number.ToString();
}
}
しっかり3で割り切れるかどうかの判定も実装できています。
ただ、元記事に書いてあるようにTDDの実践のため、あえて質素な実装に置き換えます。
namespace FizzBuzzTdd;
public class FizzBuzz
{
public string FizzBuzzFunction(int number)
{
return "Fizz";
}
}
このように実装することで、テストが通ることが確認できたかと思います。
では2つ目のテストに移りたいと思います。
[Fact(DisplayName = "5の倍数の場合はBuzzを返す")]
と記載し、提案を受けます。
[Fact(DisplayName = "5の倍数の場合はBuzzを返す")]
public void Test2()
{
//Arrange
var fizzBuzz = new FizzBuzz();
//Act
var result = fizzBuzz.FizzBuzzFunction(5);
//Assert
Assert.Equal("Buzz", result);
}
すると、先程のTest1の実装に則り、求めるテストコードが実装されました。
ここでテストを実装すると、もちろんTest2は失敗しますね。
アプリの実装を修正します。
一度先程の実装を消して、再度提案を受けます。
public string FizzBuzzFunction(int number){
if(number % 3 == 0){
return "Fizz";
}
return "Buzz";
}
良い感じですね。 テストを実行すると通ることが確認できます。
では次に3つ目のテストの実装をしていきます。
もうわざわざ [Fact(DisplayName=…)] を書かなくても以下の内容を提案してくれました。
[Fact(DisplayName = "3と5の倍数の場合はFizzBuzzを返す")]
public void Test3()
{
//Arrange
var fizzBuzz = new FizzBuzz();
//Act
var result = fizzBuzz.FizzBuzzFunction(15);
//Assert
Assert.Equal("FizzBuzz", result);
}
恐らくファイル下部に書いてあるTODO Listの内容から察してくれたのかと思います。
テストを実行すると、こちらも失敗します。
ではテストが通るようにアプリ側の実装を修正します。 アプリ側の実装において以下のような提案を受けました。
public string FizzBuzzFunction(int number){
if(number % 3 == 0 && number % 5 == 0){
return "FizzBuzz";
}
if(number % 3 == 0){
return "Fizz";
}
return "Buzz";
}
こちらも通ります。
では最後に4つ目のテストを実装します。
[Fact(DisplayName = "それ以外の場合はそのまま返す")]
public void Test4()
{
//Arrange
var fizzBuzz = new FizzBuzz();
//Act
var result = fizzBuzz.FizzBuzzFunction(7);
//Assert
Assert.Equal("7", result);
}
いいですね。
テストは想定通り失敗するので、アプリ側の実装を修正します。
…と、ここまで順調だったのですが、
なぜか最後の入力値をそのままreturnする実装だけは提案されませんでした。
Buzzをreturnする部分はif文の追加を提案してくれたのですが、最後のreturn number
の部分がどうしても提案してくれませんでした。
仕方ないので自分でreturn文を追加します。
public string FizzBuzzFunction(int number){
if(number % 3 == 0 && number % 5 == 0){
return "FizzBuzz";
}
if(number % 3 == 0){
return "Fizz";
}
if(number % 5 == 0){
return "Buzz";
}
return number; //ここだけ自前で実装
}
これですべてのテストが通るようになり、実装の方が完了です。
細かなリファクタリングの必要などはあるかと思いますが、今回は省略します。
まとめ
最後だけ若干うまくいきませんでしたが、
おおむねCopilotが正しい内容を提案してくれて実装の方をスイスイ行えた印象があります。
また、最初は提案された実装内容に誤りがありましたが、次第に解消され、質の高い提案がされていく事も実感できました。
- TODOリストを書いていること
- テストコードを書いていること
が、質の高い提案につながったように見えます。
ただGitHub CopilotとTDDの相性が良いようには感じましたが、即効性のあるものというよりじわじわ効いてくる感じだなぁと感じました。
この手法を採るためにTDDを採用する、という判断は少し行き過ぎで あくまでTDDを既に採用している方に導入をオススメするくらいかな、と個人的には感じました。
とはいいつつ、今回のような形で生成AIを使いこなし、開発効率を上げていきたいですね! また様々な手法を試してみたいと思います。
ではまた!
余談
どこかのコメントで「FizzBuzzの実装は膨大な実装がGitHubに挙がっており、通常のコードよりも学習がされてるのでは?」という意見も見られました。
確かにプログラミング学び始めはFizzBuzz問題に取り組み、GitHubなどに上げがちなので、かなりコードの学習がされているかもしれません。
もしかしたら実務においてCopilotとTDDをした場合は、こんなに質の高い提案が行われないかもしれないですね。
そのあたりも含めてまた検証する機会があれば触ってみたいと思います