Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

出力するログを検証する単体テストを追加 #815

Merged
8 changes: 4 additions & 4 deletions samples/ConsoleAppWithDI/solution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ DI コンテナーを用いた一般的な .NET の実装を、コンソール
- Maris.ConsoleApp.IntegrationTests

- テスト用の xUnit 拡張機能
- Maris.Testing
- Maris.Logging.Testing

- 利用例
- Maris.Samples.Cli
Expand Down Expand Up @@ -74,7 +74,7 @@ DI コンテナーを用いた一般的な .NET の実装を、コンソール
| src | | |
| | Maris.ConsoleApp.Core | フォルダーごと配置する |
| | Maris.ConsoleApp.Hosting | フォルダーごと配置する |
| | Maris.Testing | フォルダーごと配置する |
| | Maris.Logging.Testing | フォルダーごと配置する |
| | Directory.Build.props | |
| tests | | |
| | Maris.ConsoleApp.IntegrationTests | フォルダーごと配置する |
Expand All @@ -90,7 +90,7 @@ DI コンテナーを用いた一般的な .NET の実装を、コンソール
### ソリューションへのファイル・プロジェクトの取り込み

作成したソリューションを Visual Studio で開き、ソリューションファイルの直下に「src」ソリューションフォルダーと「tests」ソリューションフォルダーを作成します。
「src」ソリューションフォルダーに「Maris.ConsoleApp.Core」・「Maris.ConsoleApp.Hosting」・「Maris.Testing」の各プロジェクトを追加します。
「src」ソリューションフォルダーに「Maris.ConsoleApp.Core」・「Maris.ConsoleApp.Hosting」・「Maris.Logging.Testing」の各プロジェクトを追加します。
また「tests」ソリューションフォルダーに「Maris.ConsoleApp.IntegrationTests」・「Maris.ConsoleApp.UnitTests」プロジェクトを追加します。

![各プロジェクト追加後のソリューション構造](readme-images/load-projects-to-solution.png)
Expand Down Expand Up @@ -607,7 +607,7 @@ flowchart TD
Host[Maris.ConsoleApp.Hosting]
Core[Maris.ConsoleApp.Core]
UnitTests[Maris.ConsoleApp.UnitTests]
TestLib[Maris.Testing]
TestLib[Maris.Logging.Testing]
subgraph Your application
Cli-->Biz
Cli-->Infra
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using CommandLine;
using Maris.ConsoleApp.Core;
using Maris.ConsoleApp.Hosting.Resources;
using Microsoft.Extensions.Logging;

namespace Maris.ConsoleApp.Hosting;

/// <summary>
/// <see cref="ConsoleAppContext"/> のインスタンスを生成するためのファクトリクラスです。
/// </summary>
internal class ConsoleAppContextFactory
{
private readonly IApplicationProcess appProcess;
private readonly ConsoleAppSettings settings;
private readonly ILogger<ConsoleAppContextFactory> logger;

/// <summary>
/// <see cref="ConsoleAppContextFactory"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="appProcess">アプリケーションのプロセスを表すインターフェース。</param>
/// <param name="settings">コンソールアプリケーションの設定。</param>
/// <param name="logger">ロガー。</param>
/// <exception cref="ArgumentNullException">
/// <list type="bullet">
/// <item><paramref name="appProcess"/> が <see langword="null"/> です。</item>
/// <item><paramref name="settings"/> が <see langword="null"/> です。</item>
/// <item><paramref name="logger"/> が <see langword="null"/> です。</item>
/// </list>
/// </exception>
public ConsoleAppContextFactory(IApplicationProcess appProcess, ConsoleAppSettings settings, ILogger<ConsoleAppContextFactory> logger)
{
this.appProcess = appProcess ?? throw new ArgumentNullException(nameof(appProcess));
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

/// <summary>
/// コンソールアプリケーションの実行コンテキストを生成します。
/// 単体テスト用に公開しています。
/// </summary>
/// <param name="args">コンソールアプリケーションの起動引数。</param>
/// <param name="commandParametersOption">
/// コマンドの名前とコマンドの型を管理するコレクションのオプション設定を実行します。
/// 既定値は <see langword="null"/> です。
/// </param>
/// <returns>生成した <see cref="CreateConsoleAppContext"/> 。</returns>
/// <exception cref="InvalidOperationException">
/// <list type="bullet">
/// <item>コマンドパラメーターの型が見つかりません。</item>
/// </list>
/// </exception>
internal ConsoleAppContext CreateConsoleAppContext(
IEnumerable<string> args,
Action<CommandParameterTypeCollection>? commandParametersOption)
{
this.logger.LogInformation(Messages.ParseParameter.Embed(string.Join(' ', args)));
var commandParameterTypes = new CommandParameterTypeCollection();
if (commandParametersOption is null)
{
commandParameterTypes.InitializeFromAllAssemblies();
}
else
{
commandParametersOption(commandParameterTypes);
}

if (!commandParameterTypes.Any())
{
var assemblies = string.Join(',', commandParameterTypes.LoadedAssemblies.Select(asm => asm.GetName().Name));
throw new InvalidOperationException(
Messages.CommandParameterIsNotExists.Embed(typeof(CommandAttribute), assemblies));
}

var param = Parser.Default.ParseArguments(args, commandParameterTypes.ToArray());
if (param is null || param.Tag == ParserResultType.NotParsed)
{
this.appProcess.Exit(this.settings.DefaultValidationErrorExitCode);
}

return new ConsoleAppContext(param.Value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static IServiceCollection AddConsoleAppService(this IServiceCollection se
services.AddHostedService<ConsoleAppHostedService>();
services.AddSingleton<IApplicationProcess, ConsoleAppProcess>();
services.AddConsoleAppSettings(options);
services.AddSingleton<ConsoleAppContextFactory>();
services.AddConsoleAppContext(args);
services.AddSingleton<CommandExecutor>();
services.AddSingleton<ICommandManager, DefaultCommandManager>();
Expand Down Expand Up @@ -76,35 +77,8 @@ internal static IServiceCollection AddConsoleAppSettings(this IServiceCollection
/// <returns>サービスを追加済みのサービスコレクション。</returns>
internal static IServiceCollection AddConsoleAppContext(this IServiceCollection services, IEnumerable<string> args, Action<CommandParameterTypeCollection>? commandParametersOption = null)
=> services.AddSingleton(provider =>
{
var logger = provider.GetService<ILoggerFactory>()?.CreateLogger(typeof(ServiceCollectionExtensions));
logger?.LogInformation(Messages.ParseParameter.Embed(string.Join(' ', args)));

var settings = provider.GetRequiredService<ConsoleAppSettings>();
var appProcess = provider.GetRequiredService<IApplicationProcess>();
var commandParameterTypes = new CommandParameterTypeCollection();
if (commandParametersOption is null)
{
commandParameterTypes.InitializeFromAllAssemblies();
}
else
{
commandParametersOption(commandParameterTypes);
}

if (!commandParameterTypes.Any())
{
var assemblies = string.Join(',', commandParameterTypes.LoadedAssemblies.Select(asm => asm.GetName().Name));
throw new InvalidOperationException(
Messages.CommandParameterIsNotExists.Embed(typeof(CommandAttribute), assemblies));
}

var param = Parser.Default.ParseArguments(args, commandParameterTypes.ToArray());
if (param is null || param.Tag == ParserResultType.NotParsed)
{
appProcess.Exit(settings.DefaultValidationErrorExitCode);
}

return new ConsoleAppContext(param.Value);
});
{
var factory = provider.GetRequiredService<ConsoleAppContextFactory>();
return factory.CreateConsoleAppContext(args, commandParametersOption);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using CommandLine;
using Maris.ConsoleApp.Core;
using Maris.ConsoleApp.Hosting;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace Maris.ConsoleApp.UnitTests.Hosting;

public class ConsoleAppContextFactoryTest(ITestOutputHelper testOutputHelper) : TestBase(testOutputHelper)
{
[Fact]
public void Constructor_IApplicationProcessがnull_ArgumentNullExceptionが発生する()
{
// Arrange
IApplicationProcess? appProcess = null;
var settings = new ConsoleAppSettings();
var logger = this.CreateTestLogger<ConsoleAppContextFactory>();

// Act
var action = () => new ConsoleAppContextFactory(appProcess!, settings, logger);

// Assert
Assert.Throws<ArgumentNullException>("appProcess", action);
}

[Fact]
public void Constructor_ConsoleAppSettingsがnull_ArgumentNullExceptionが発生する()
{
// Arrange
var appProcess = new TestApplicationProcess();
ConsoleAppSettings? settings = null;
var logger = this.CreateTestLogger<ConsoleAppContextFactory>();

// Act
var action = () => new ConsoleAppContextFactory(appProcess, settings!, logger);

// Assert
Assert.Throws<ArgumentNullException>("settings", action);
}

[Fact]
public void Constructor_ILoggerがnull_ArgumentNullExceptionが発生する()
{
// Arrange
var appProcess = new TestApplicationProcess();
var settings = new ConsoleAppSettings();
ILogger<ConsoleAppContextFactory>? logger = null;

// Act
var action = () => new ConsoleAppContextFactory(appProcess, settings, logger!);

// Assert
Assert.Throws<ArgumentNullException>("logger", action);
}

[Fact]
public void CreateConsoleAppContext_起動パラメーターが情報レベルでログに出力される()
{
// Arrange
var args = new string[] { "console-app-context-factory-test", "--category-id", "123" };
var types = new Type[] { typeof(TestParameter) };
var assembly = new TestAssembly(types);
Action<CommandParameterTypeCollection>? commandParametersOption = collection => collection.AddCommandParameterTypeFrom(assembly);
var appProcess = new TestApplicationProcess();
var settings = new ConsoleAppSettings();
var logger = this.CreateTestLogger<ConsoleAppContextFactory>();
var factory = new ConsoleAppContextFactory(appProcess, settings, logger);

// Act
_ = factory.CreateConsoleAppContext(args, commandParametersOption);

// Assert
Assert.Equal(1, this.LogCollector.Count);
var record = this.LogCollector.LatestRecord;
Assert.Equal(LogLevel.Information, record.Level);
Assert.Equal(default, record.Id);
Assert.Equal("起動パラメーター:console-app-context-factory-test --category-id 123 のパースを行います。", record.Message);
}

[Fact]
public void CreateConsoleAppContext_正常系_起動パラメーターの情報が含まれたコンテキストを生成できる()
{
// Arrange
var args = new string[] { "console-app-context-factory-test", "--category-id", "123" };
var types = new Type[] { typeof(TestParameter) };
var assembly = new TestAssembly(types);
Action<CommandParameterTypeCollection>? commandParametersOption = collection => collection.AddCommandParameterTypeFrom(assembly);
var appProcess = new TestApplicationProcess();
var settings = new ConsoleAppSettings();
var logger = this.CreateTestLogger<ConsoleAppContextFactory>();
var factory = new ConsoleAppContextFactory(appProcess, settings, logger);

// Act
var context = factory.CreateConsoleAppContext(args, commandParametersOption);

// Assert
var parameter = Assert.IsType<TestParameter>(context.Parameter);
Assert.Equal(123, parameter.CategoryId);
}

[Fact]
public void CreateConsoleAppContext_読み込んだアセンブリ内にCommandAttributeを付与したパラメーターがない_InvalidOperationExceptionが発生する()
{
// Arrange
var args = new string[] { "console-app-context-factory-test", "--category-id", "123" };
Type[] types = [];
var assembly = new TestAssembly(types);
Action<CommandParameterTypeCollection>? commandParametersOption = collection => collection.AddCommandParameterTypeFrom(assembly);
var appProcess = new TestApplicationProcess();
var settings = new ConsoleAppSettings();
var logger = this.CreateTestLogger<ConsoleAppContextFactory>();
var factory = new ConsoleAppContextFactory(appProcess, settings, logger);

// Act
var action = () => factory.CreateConsoleAppContext(args, commandParametersOption);

// Assert
var ex = Assert.Throws<InvalidOperationException>(action);
Assert.Equal("Maris.ConsoleApp.Core.CommandAttribute 属性を追加したコマンドパラメーターの型が読み込まれたアセンブリ TestAssembly に見つかりません。", ex.Message);
}

[Fact]
public void CreateConsoleAppContext_起動パラメーターのパースに失敗する_DefaultValidationErrorExitCodeに設定した終了コードでプロセスが終了する()
{
// Arrange
var args = new string[] { "console-app-context-factory-test", "--category-id", "invalid-value" };
var types = new Type[] { typeof(TestParameter) };
var assembly = new TestAssembly(types);
Action<CommandParameterTypeCollection>? commandParametersOption = collection => collection.AddCommandParameterTypeFrom(assembly);
var appProcess = new TestApplicationProcess();
var exitCode = 789;
var settings = new ConsoleAppSettings { DefaultValidationErrorExitCode = exitCode };
var logger = this.CreateTestLogger<ConsoleAppContextFactory>();
var factory = new ConsoleAppContextFactory(appProcess, settings, logger);

// Act
var action = () => factory.CreateConsoleAppContext(args, commandParametersOption);

// Assert
var ex = Assert.Throws<ApplicationException>(action);
Assert.Equal("アプリケーションの終了処理が呼び出されました。", ex.Message);
Assert.Equal(exitCode, appProcess.ExitCode);
}

private class TestAssembly : Assembly
{
private readonly Type[] types;

internal TestAssembly(Type[] types) => this.types = types;

public override Type[] GetTypes() => this.types;

public override AssemblyName GetName() => new(nameof(TestAssembly));
}

[Command("console-app-context-factory-test", typeof(TestCommand))]
private class TestParameter
{
[Option("category-id", Required = true)]
public long CategoryId { get; set; }
}

private class TestCommand : SyncCommand<TestParameter>
{
protected internal override ICommandResult Execute(TestParameter parameter)
=> new SuccessResult();
}

private class TestApplicationProcess : IApplicationProcess
{
public int ExitCode { get; private set; }

[DoesNotReturn]
public void Exit(int exitCode)
{
this.ExitCode = exitCode;
throw new ApplicationException("アプリケーションの終了処理が呼び出されました。");
}
}
}
Loading