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クラスの単体テストを容易に行うことができます。インタフェースを使用することで、依存関係の注入が可能になり、モックオブジェクトを使用してテストを行うことができます。これにより、テストの柔軟性と保守性が向上します。