Open2
C#で単体試験(xUnit)を見据えた実装について
C#ソースコードでxUnitを用いた単体試験のし易さを考慮すると、どのような実装をするべきか?
前提
以下のような自作のOpenAIServiceクラスを使って、トークン残量を計算するトークン計算クラスの関数テスト(UnitTest)を考える。
このトークン計算クラスは、OpenAIServiceクラスの関数を使用するため、試験を実施する際には該当する関数の模擬が必要となる。
■テスト対象のクラス関数
/// <summary>
/// TokenCalculator クラスは、OpenAI サービスを使用してトークンの計算を行います。
/// </summary>
public class TokenCalculator
{
private readonly OpenAiService _openAiService; // 自作のOpenAIサービス操作用のクラス
private readonly ILogger<TokenCalculator> _logger;
/// <summary>
/// TokenCalculator クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="openAiService">トークンのカウントを行うための OpenAI サービス。</param>
/// <param name="logger">ログを記録するためのロガー。</param>
public TokenCalculator(OpenAiService openAiService, ILogger<TokenCalculator> logger)
{
_openAiService = openAiService;
_logger = logger;
}
/// <summary>
/// 指定されたプロンプト文字列に基づいて、残りのトークン数を計算します。
/// </summary>
/// <param name="promptString">トークンをカウントするためのプロンプト文字列。</param>
/// <param name="enableUseToken">使用可能なトークンの初期数。</param>
/// <returns>計算後の残りのトークン数。</returns>
public int CalculateRemainingTokens(string promptString, int enableUseToken)
{
int tokenCount = _openAiService.CountToken(promptString);
enableUseToken -= tokenCount;
_logger.LogInformation("tokenCount : {Count}, RemainingToken : {enableUseToken}", tokenCount, enableUseToken);
return enableUseToken;
}
}
■テスト対象に使用される(模擬が必要となる)関数
public class OpenAiService
{
public int CountToken(string inputText)
{
var tokenizer = Tokenizer.CreateTiktokenForModel("gpt-4");
var numberOfToken = tokenizer.CountTokens(inputText);
return numberOfToken;
}
}
単体テストのし易さを考慮すると、インタフェースを導入する方法が最も一般的であり、保守性や可読性の観点からも推奨される
以下に、インタフェースを導入した実装例を示します。
- ステップ1: インタフェースの作成
まず、OpenAiServiceのインタフェースを作成します。
public interface IOpenAiService
{
int CountToken(string inputText);
}
- ステップ2: OpenAiServiceクラスの実装
次に、OpenAiServiceクラスがこのインタフェースを実装するようにします。
public class OpenAiService : IOpenAiService
{
public int CountToken(string inputText)
{
var tokenizer = Tokenizer.CreateTiktokenForModel("gpt-4");
var numberOfToken = tokenizer.CountTokens(inputText);
return numberOfToken;
}
}
- ステップ3: TokenCalculatorクラスの依存関係注入
TokenCalculatorクラスにIOpenAiServiceを注入します。
public class TokenCalculator
{
private readonly IOpenAiService _openAiService;
private readonly ILogger<TokenCalculator> _logger;
public TokenCalculator(IOpenAiService openAiService, ILogger<TokenCalculator> logger)
{
_openAiService = openAiService;
_logger = logger;
}
public int CalculateRemainingTokens(string promptString, int enableUseToken)
{
int tokenCount = _openAiService.CountToken(promptString);
enableUseToken -= tokenCount;
_logger.LogInformation("tokenCount : {Count}, RemainingToken : {enableUseToken}", tokenCount, enableUseToken);
return enableUseToken;
}
}
- ステップ4: xUnitを用いた単体テストの作成
最後に、xUnitを用いてTokenCalculatorクラスの単体テストを作成します。
public class TokenCalculatorTests
{
[Fact]
public void CalculateRemainingTokens_ShouldReturnCorrectRemainingTokens()
{
// Arrange
var mockOpenAiService = new Mock<IOpenAiService>();
mockOpenAiService.Setup(s => s.CountToken(It.IsAny<string>())).Returns(10);
var mockLogger = new Mock<ILogger<TokenCalculator>>();
var tokenCalculator = new TokenCalculator(mockOpenAiService.Object, mockLogger.Object);
// Act
int remainingTokens = tokenCalculator.CalculateRemainingTokens("some input text", 100);
// Assert
Assert.Equal(90, remainingTokens);
}
}
この方法により、OpenAiServiceの実装を変更することなく、TokenCalculatorクラスの単体テストを容易に行うことができます。インタフェースを使用することで、依存関係の注入が可能になり、モックオブジェクトを使用してテストを行うことができます。これにより、テストの柔軟性と保守性が向上します。