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

現在日時の生成にTimeProviderを利用する #862

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
47524b8
Order、ConsoleAppHostedServiceでTimeProviderを使用するよう修正
KentaHizume Feb 27, 2024
2b7c377
ObjectStateについてTimeProviderを使用するよう修正
KentaHizume Feb 27, 2024
d8bffdb
FakeTimeProviderを使用して日時を設定するよう修正
KentaHizume Feb 27, 2024
b1cd4bc
TimeProvderを使用してCookieの有効期限を設定するよう修正
KentaHizume Feb 27, 2024
6ceb8cc
Merge branch 'main' into feature/現在日時の生成にTimeProviderを利用する
KentaHizume Feb 27, 2024
d79cbe6
ObjectStateの実装の誤りを修正
KentaHizume Feb 27, 2024
7604c90
csprojへの不要な変更を削除
KentaHizume Feb 28, 2024
a658b33
期待値の日付を所定の書式にフォーマットしてから比較するように修正
KentaHizume Feb 28, 2024
10b10c3
テスト中にAdvanceを行う時間を1000ミリ秒に増加
KentaHizume Feb 28, 2024
5c8c3a0
TimeProviderをDIコンテナに登録せず、internalコンストラクターを使用するように修正
KentaHizume Feb 28, 2024
d918745
Orderについてpublicコンストラクタからinternalコンストラクタを呼び出すように修正
KentaHizume Feb 28, 2024
8b6cd83
BuyerIdFilterAttributeについてinternalコンストラクタを呼び出すように修正
KentaHizume Feb 28, 2024
020f577
Merge branch 'main' into feature/現在日時の生成にTimeProviderを利用する
KentaHizume Feb 29, 2024
b82b80b
internalコンストラクタの冗長なデフォルト引数を削除
KentaHizume Feb 29, 2024
d3d6a56
BuyerIdFilterAttributeのXMLコメントの不整合を修正
KentaHizume Feb 29, 2024
8c69c92
XMLコメントの誤記を修正
KentaHizume Mar 1, 2024
2af3ce5
internalメソッドの引数に対してもNULLチェックを行うよう修正
KentaHizume Mar 1, 2024
d841d80
Revert "internalメソッドの引数に対してもNULLチェックを行うよう修正"
KentaHizume Mar 1, 2024
da32f4f
timeProviderに対するNULLチェックを追加
KentaHizume Mar 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions samples/ConsoleAppWithDI/solution/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.2.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics;
using Maris.ConsoleApp.Core;
using Maris.ConsoleApp.Core;
using Maris.ConsoleApp.Hosting.Resources;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
Expand All @@ -15,7 +14,8 @@ internal class ConsoleAppHostedService : IHostedService
private readonly ConsoleAppSettings settings;
private readonly CommandExecutor executor;
private readonly ILogger logger;
private readonly Stopwatch stopwatch = new();
private readonly TimeProvider timeProvider;
private long startTime;

/// <summary>
/// <see cref="ConsoleAppHostedService"/> クラスの新しいインスタンスを初期化します。
Expand All @@ -33,11 +33,34 @@ internal class ConsoleAppHostedService : IHostedService
/// </list>
/// </exception>
public ConsoleAppHostedService(IHostApplicationLifetime lifetime, ConsoleAppSettings settings, CommandExecutor executor, ILogger<ConsoleAppHostedService> logger)
: this(lifetime, settings, executor, logger, TimeProvider.System)
{
}

/// <summary>
/// <see cref="ConsoleAppHostedService"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="lifetime">アプリケーションのライフタイムイベントを通知できるようにするためのオブジェクト</param>
/// <param name="settings">コンソールアプリケーションの設定項目を管理するオブジェクト。</param>
/// <param name="executor">コマンドの実行を管理するオブジェクト。</param>
/// <param name="logger">ロガー</param>
/// <param name="timeProvider">日時のプロバイダ。</param>
/// <exception cref="ArgumentNullException">
/// <list type="bullet">
/// <item><paramref name="lifetime"/> が <see langword="null"/> です。</item>
/// <item><paramref name="settings"/> が <see langword="null"/> です。</item>
/// <item><paramref name="executor"/> が <see langword="null"/> です。</item>
/// <item><paramref name="logger"/> が <see langword="null"/> です。</item>
/// <item><paramref name="timeProvider"/> が <see langword="null"/> です。</item>
/// </list>
/// </exception>
internal ConsoleAppHostedService(IHostApplicationLifetime lifetime, ConsoleAppSettings settings, CommandExecutor executor, ILogger<ConsoleAppHostedService> logger, TimeProvider timeProvider)
{
this.lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
this.executor = executor ?? throw new ArgumentNullException(nameof(executor));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}

/// <summary>
Expand All @@ -54,7 +77,7 @@ public ConsoleAppHostedService(IHostApplicationLifetime lifetime, ConsoleAppSett
/// <returns>タスク。</returns>
public async Task StartAsync(CancellationToken cancellationToken)
{
this.stopwatch.Start();
this.startTime = this.timeProvider.GetTimestamp();
this.logger.LogInformation(Events.StartHostingService, Messages.StartHostingService, this.executor.CommandName);
try
{
Expand Down Expand Up @@ -85,13 +108,12 @@ public async Task StartAsync(CancellationToken cancellationToken)
/// <returns>タスク。</returns>
public Task StopAsync(CancellationToken cancellationToken)
{
this.stopwatch.Stop();
this.logger.LogInformation(
Events.StopHostingService,
Messages.StopHostingService,
this.executor.CommandName,
Environment.ExitCode,
this.stopwatch.ElapsedMilliseconds);
this.timeProvider.GetElapsedTime(this.startTime).TotalMilliseconds);
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

internal class ObjectState
{
internal ObjectState(Guid objectId, Type objectType, Condition condition)
private readonly TimeProvider timeProvider;
private DateTime createDate;

internal ObjectState(Guid objectId, Type objectType, Condition condition, TimeProvider timeProvider)
{
this.ObjectId = objectId;
this.ObjectType = objectType;
this.Condition = condition;
this.timeProvider = timeProvider;
this.createDate = this.timeProvider.GetLocalNow().UtcDateTime;
}

internal Guid ObjectId { get; private set; }
Expand All @@ -15,5 +20,5 @@ internal ObjectState(Guid objectId, Type objectType, Condition condition)

internal Condition Condition { get; private set; }

internal DateTime CreateDate { get; } = DateTime.Now;
internal DateTime CreateDate { get => this.createDate; }
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
namespace Maris.ConsoleApp.IntegrationTests.ScopeTests;
using Microsoft.Extensions.Time.Testing;

namespace Maris.ConsoleApp.IntegrationTests.ScopeTests;

internal class TestObjectBase : IDisposable
{
private readonly Guid objectId = Guid.NewGuid();
private bool disposed;
private TimeProvider fakeTimeProvider = new FakeTimeProvider();

public TestObjectBase() =>
public TestObjectBase()
{
ObjectStateHistory.Add(new(
this.objectId,
this.GetType(),
Condition.Creating));
Condition.Creating,
this.fakeTimeProvider));
}

public void Dispose()
{
Expand All @@ -21,16 +27,17 @@ protected void LogHistory() =>
ObjectStateHistory.Add(new(
this.objectId,
this.GetType(),
this.disposed ? Condition.AlreadyDisposed : Condition.Alive));
this.disposed ? Condition.AlreadyDisposed : Condition.Alive,
this.fakeTimeProvider));

protected virtual void Dispose(bool disposing)
{
ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposing));
ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposing, this.fakeTimeProvider));

if (!this.disposed)
{
this.disposed = true;
ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposed));
ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposed, this.fakeTimeProvider));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Maris.ConsoleApp.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
using Xunit.Abstractions;

namespace Maris.ConsoleApp.UnitTests.Hosting;
Expand Down Expand Up @@ -386,6 +387,37 @@ public async Task StopAsync_コマンドの実行完了後に呼び出す_コマ
Assert.StartsWith($"sync-command コマンドのホストの処理が終了コード {exitCode} で完了しました。", record.Message);
}

[Fact]
public async Task StopAsync_コマンドの実行時間がログに出力される()
{
// Arrange
var lifetime = Mock.Of<IHostApplicationLifetime>();
var settings = new ConsoleAppSettings();
var commandAttribute = new CommandAttribute("sync-command", typeof(SyncCommandImpl));
var parameter = new CommandParameter();
var context = new ConsoleAppContext(commandAttribute, parameter);
var exitCode = 456;
var command = new SyncCommandImpl(exitCode);
command.Initialize(context);
var managerMock = CreateCommandManagerMock(command);
var manager = managerMock.Object;
var commandExecutorLogger = this.CreateTestLogger<CommandExecutor>();
var executor = new CommandExecutor(manager, commandExecutorLogger);
var logger = this.CreateTestLogger<ConsoleAppHostedService>();
var fakeTimeProvider = new FakeTimeProvider();
var service = new ConsoleAppHostedService(lifetime, settings, executor, logger, fakeTimeProvider);
var cancellationToken = new CancellationToken(false);
await service.StartAsync(cancellationToken);

// Act
fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000));
await service.StopAsync(cancellationToken);

// Assert
var record = this.LogCollector.LatestRecord;
Assert.Contains($"実行時間は 1000 ms でした。", record.Message);
}

private static Mock<ICommandManager> CreateCommandManagerMock(CommandBase? creatingCommand = null)
{
var command = creatingCommand ?? new TestCommand();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="StyleCop.Analyzers">
Expand Down
1 change: 1 addition & 0 deletions samples/Dressca/dressca-backend/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.2.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.2.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.0.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class Order
{
private readonly List<OrderItem> orderItems = new();
private readonly Account? account;
private readonly TimeProvider timeProvider;
private string? buyerId;
private ShipTo? shipToAddress;

Expand All @@ -31,6 +32,31 @@ public class Order
/// </list>
/// </exception>
public Order(string buyerId, ShipTo shipToAddress, List<OrderItem> orderItems)
: this(buyerId, shipToAddress, orderItems, TimeProvider.System)
{
}

/// <summary>
/// <see cref="Order"/> クラスの新しいインスタンスを初期化します。
/// 単体テスト用に<see cref="TimeProvider"/> を受け取ることができます。
/// </summary>
/// <param name="buyerId">購入者 Id 。</param>
/// <param name="shipToAddress">配送先住所。</param>
/// <param name="orderItems">注文アイテムのリスト。</param>
/// <param name="timeProvider">日時のプロバイダ。通常はシステム日時。</param>
/// <exception cref="ArgumentException">
/// <list type="bullet">
/// <item><paramref name="buyerId"/> が <see langword="null"/> または空の文字列です。</item>
/// <item><paramref name="orderItems"/> が <see langword="null"/> または空のリストです。</item>
/// </list>
/// </exception>
/// <exception cref="ArgumentNullException">
/// <list type="bullet">
/// <item><paramref name="shipToAddress"/> が <see langword="null"/> です。</item>
/// <item><paramref name="timeProvider"/> が <see langword="null"/> です。</item>
/// </list>
/// </exception>
internal Order(string buyerId, ShipTo shipToAddress, List<OrderItem> orderItems, TimeProvider timeProvider)
{
if (orderItems is null || !orderItems.Any())
{
Expand All @@ -46,11 +72,14 @@ public Order(string buyerId, ShipTo shipToAddress, List<OrderItem> orderItems)
this.DeliveryCharge = this.account.GetDeliveryCharge();
this.ConsumptionTax = this.account.GetConsumptionTax();
this.TotalPrice = this.account.GetTotalPrice();
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
this.OrderDate = this.timeProvider.GetLocalNow();
}

private Order()
{
// Required by EF Core.
this.timeProvider = TimeProvider.System;
}

/// <summary>
Expand Down Expand Up @@ -81,7 +110,7 @@ private set
/// 注文日を取得します。
/// このクラスのインスタンスが生成されたシステム日時が自動的に設定されます.
/// </summary>
public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
public DateTimeOffset OrderDate { get; private set; }

/// <summary>
/// お届け先を取得します。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,34 @@ public class BuyerIdFilterAttribute : ActionFilterAttribute
{
private const string DefaultBuyerIdCookieName = "Dressca-Bid";
private readonly string buyerIdCookieName;
private readonly TimeProvider timeProvider;

/// <summary>
/// <see cref="BuyerIdFilterAttribute"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="buyerIdCookieName">Cookie のキー名。未指定時は "Dressca-Bid" 。</param>
public BuyerIdFilterAttribute(string buyerIdCookieName = DefaultBuyerIdCookieName)
: this(buyerIdCookieName, TimeProvider.System)
{
}

/// <summary>
/// <see cref="BuyerIdFilterAttribute"/> クラスの新しいインタンスを初期化します。
/// 単体テスト用に<see cref="TimeProvider"/> を受け取ることができます。
/// </summary>
/// <param name="buyerIdCookieName">Cookie のキー名。</param>
/// <param name="timeProvider">日時のプロバイダ。通常はシステム日時。</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="buyerIdCookieName"/> が <see langword="null"/> です。
/// <list type="bullet">
/// <paramref name="buyerIdCookieName"/> が <see langword="null"/> です。
/// <paramref name="timeProvider"/> が <see langword="null"/> です。
/// </list>
/// </exception>
public BuyerIdFilterAttribute(string buyerIdCookieName = DefaultBuyerIdCookieName)
=> this.buyerIdCookieName = buyerIdCookieName ?? throw new ArgumentNullException(nameof(buyerIdCookieName));
internal BuyerIdFilterAttribute(string buyerIdCookieName, TimeProvider timeProvider)
{
this.buyerIdCookieName = buyerIdCookieName ?? throw new ArgumentNullException(nameof(buyerIdCookieName));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}

/// <inheritdoc/>
public override void OnActionExecuting(ActionExecutingContext context)
Expand Down Expand Up @@ -61,7 +79,7 @@ public override void OnActionExecuted(ActionExecutedContext context)
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Secure = true,
Expires = DateTimeOffset.Now.AddDays(7),
Expires = this.timeProvider.GetLocalNow().AddDays(7),
});

base.OnActionExecuted(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Dressca.UnitTests" />
</ItemGroup>

<Target Name="NSwag" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Debug'">
<Exec WorkingDirectory="$(ProjectDir)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development" Command="$(NSwagExe_Net80) run nswag.json /variables:Configuration=$(Configuration)" />
</Target>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Dressca.ApplicationCore.Ordering;
using Microsoft.Extensions.Time.Testing;

namespace Dressca.UnitTests.ApplicationCore.Ordering;

Expand Down Expand Up @@ -171,6 +172,24 @@ public void HasMatchingBuyerId_指定の購入者Idと一致しない_false()
Assert.False(result);
}

[Fact]
public void Constructor_OrderDateが注文時のシステム時刻と等しい()
{
// Arrange
var buyerId = Guid.NewGuid().ToString("D");
var shipTo = CreateDefaultShipTo();
var items = CreateDefaultOrderItems();
var fakeTimeProvider = new FakeTimeProvider();
var testOrderTime = new DateTimeOffset(2024, 4, 1, 00, 00, 00, new TimeSpan(9, 0, 0));
fakeTimeProvider.SetUtcNow(testOrderTime);

// Act
var order = new Order(buyerId, shipTo, items, fakeTimeProvider);

// Assert
Assert.Equal(testOrderTime, order.OrderDate);
}

private static Address CreateDefaultAddress()
{
const string defaultPostalCode = "100-8924";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="StyleCop.Analyzers">
Expand Down
Loading