プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2019/01/08

ASP.NET Core の多言語対応用リソースファイルのテスト

event_note2019/01/08 0:07

ASP.NET Core のおいて、多言語対応のためのリソースファイルが正しいかどうかをユニットテストで確認したいと思い、試行錯誤した結果です。

ASP.NET Core の多言語対応の機能を使っていることが前提です。

環境

  • Visual Studio 2017
  • ASP.NET Core 2.1
  • xUnit 2.4.1

やりたいこと

Localizer に指定されたキーが、リソースファイルにあるかどうか

  • キーがリソースファイルに存在せず、キーの内容がそのまま出力されている場合にテスト失敗としたい
  • 例えば、あるキーが日本語のリソースファイルにはあるけど中国語のリソースファイルにはなく、言語が中国語の場合にはキーがそのまま出力されてしまっている、という場合にテスト失敗にしたい

これにより、リソースファイルへのキーの追加忘れや、ソースファイルの変更に伴うリソースファイルの修正忘れをチェックできます。

未使用のキーがないか

  • リソースファイルにキーが定義されているが、実際にソースコード上で使われていないキーがある場合にはテスト失敗としたい

これにより、リソースファイル内の不要なキーをチェックできます。

手法と概要

いろいろ悩んだ結果、以下のやり方しか思いつきませんでした。

  • ユニットテストを実行する前にテスト対象プロジェクト内のソースコードを全検索し、localizer に指定されているキーを正規表現により抽出する
  • ソースコードから抽出したキーとリソースファイル内のキーを比較
  • これを全カルチャーのリソースファイルに対してチェックする

準備

テスト対象プロジェクト内のソースコードからキーを取得

テスト対象プロジェクト内のソースコードを全検索し、localizer に指定されているキーを正規表現により抽出します。
対象とするソースファイルの拡張子は .cs.cshtml です。

まずはソースファイルの一覧を以下のようにして取得します。

var files = GetFiles("テスト対象プロジェクトへのパス", ".cs", ".cshtml");
IEnumerable<string> GetFiles(string path, params string[] extensions)
{
    return Directory
        .GetFiles(path, "*.*", SearchOption.AllDirectories)
        .Where(c => extensions.Any(extension => c.EndsWith(extension)))
        .ToArray();
}

ソースファイルの一覧を取得したら、各ソースファイル内から以下のようにしてキーを取得します。

// 各ソースファイル内の localizer のキーを取得
var allKeys = new List<string>();
foreach (var file in files)
{
    allKeys.AddRange(GetAllKeys(file));
}
// 重複を除外
SourceKeys = allKeys.Distinct();
IEnumerable<string> GetAllKeys(string path)
{
    var keys = new List<string>();

    // 正規表現で localizer[] に指定されたキーを抽出する
    var rgx = new Regex("localizer\\[\"(.*)\"\\]", RegexOptions.IgnoreCase);

    using (var file = new StreamReader(path))
    {
        var line = "";

        while ((line = file.ReadLine()) != null)
        {
            var match = rgx.Match(line);
            if (match.Success)
            {
                keys.Add(match.Groups[1].Value);
            }
        }
    }

    return keys;
}

リソースファイル内のキーを取得するため、DI の準備を行う

リソースファイル内のキーを取得するために、きちんと DI サービスの設定を行って Localizer を取得します。
やっていることは ASP.NET Core プロジェクトの Startup.cs で行う DI サービスの設定と全く同じです。

IServiceCollection serviceCollection = new ServiceCollection();

// DI の準備を行う
// 何故か ILoggerFactory を DI しないと Localizer も DI できなかった
ILoggerFactory loggerFactory = new LoggerFactory().AddConsole();
services.AddSingleton(loggerFactory);
services.AddLogging();

// ローカリゼーションを行うために必要なサービスをコンテナに登録
services.AddLocalization(options => options.ResourcesPath = "Resources");

// 参考: https://github.com/aspnet/Entropy/blob/master/samples/Localization.StarterWeb/Startup.cs
const string enCulture = "en";
services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
        new CultureInfo(enCulture),
        new CultureInfo("ja"),
    };
    options.DefaultRequestCulture = new RequestCulture(culture: enCulture, uiCulture: enCulture);
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
});
            
// DI サービスのビルド
serviceProvider = serviceCollection.BuildServiceProvider();
// サービスの取得
Localizer = serviceProvider.GetService<IStringLocalizer<Resource>>();

テストコード

以上の準備が終わったうえで、例えば以下のようにテストします。

ソースファイル内のキーがリソースファイルに定義されているかどうか

[Theory,
    InlineData("ja"),
    InlineData("")]
public void KeyDefined(string culture)
{
    bool error = false;
    // 指定したカルチャーのリソースファイルのキーを取得
    var resourceKeys = Localizer.WithCulture(new CultureInfo(culture)).GetAllStrings(false).Select(x => x.Name);

    // ソースファイル内のキーがリソースファイルに定義されているかどうかチェック
    foreach (var key in SourceKeys)
    {
        try
        {
            Assert.True(resourceKeys.Contains(key));
        }
        catch
        {
            // 都度テスト失敗にしていたら、キーをリソースファイルの追加していくのが大変なので、
            // 足りないキーを一括で表示したうえで、最後にテスト失敗にする
            output.WriteLine($"culture: {culture}\nkey: {key}\n-----");
            error = true;
        }
    }
    Assert.False(error);
}

リソースファイルに未使用のキーがあるかどうか

[Theory,
    InlineData("ja"),
    InlineData("")]
public void UnusedKey(string culture)
{
    bool error = false;
    // 指定したカルチャーのリソースファイルのキーを取得
    var resourceKeys = Localizer.WithCulture(new CultureInfo(culture)).GetAllStrings(false).Select(x => x.Name);

    // リソースファイル内のキーがソースファイル上にあるかどうかチェック
    foreach (var key in resourceKeys)
    {
        try
        {
            Assert.True(SourceKeys.Contains(key));
        }
        catch
        {
            // 都度テスト失敗にしていたら、キーをリソースファイルから削除していくのが大変なので、
            // 未使用キーを一括で表示したうえで、最後にテスト失敗にする
            output.WriteLine($"culture: {culture}\nkey: {key}\n----------");
            error = true;
        }
    }

    Assert.False(error);
}

チェックできないこと

以上で一応やりたいことはできました。
しかし、これは ASP.NET Core の多言語対応の書き方を正しく行っていることが前提となります。

つまり、Localizer["key"] と書かなければならないところを、間違って直接 "key" と書いてしまっているような場合はチェックできません。