DateTime
を使用するコードをテスト可能なイイ感じにします。
テストができないコード
DateTime
が挙動にかかわるとユニットテストがしにくくなります。
時刻に応じて挨拶を返すサービスクラスを例に考えてみます。
public class GreetingService
{
public string Greet(string name)
{
var time = DateTime.Now.TimeOfDay;
if (new TimeSpan(5, 00, 00) <= time && time < new TimeSpan(10, 00, 00))
return $"おはよう!{name}さん!";
else if (new TimeSpan(10,00,00) <= time && time < new TimeSpan(17, 00, 00))
return $"こんにちは!{name}さん!";
else
return $"こんばんは!{name}さん!";
}
}
現在時刻に応じて正しく文字列が返るかテストがしたいけど、できない...
これでは幸せになれません...。
DI(依存性注入)できるようにリファクタリング
dependency injection(依存性注入)はとても大雑把に説明すると、依存するオブジェクトをクラスの外から注入することです。
外から注入することで、動的に動作を変えられるようになります。
今回の場合、 DateTime
が GreetingService
の外から変えられればいいわけですね。
まず、IDateTime
インタフェースを定義します。
DI の詳細の記事ではないので省略しますが、実装を隠ぺいするためです。
GreetingService
が具体的な実装ではなく、 IDateTime
インタフェースのみに依存するようにします。
public interface IDateTime
{
DateTime Now { get; }
}
次に IDateTime
を実装したクラスを作成します。
今回はサーバーシステムの現在時刻を返すクラスを作成します。
public class ServerDateTime : IDateTime
{
public DateTime Now => DateTime.Now;
}
最後に GreetingService
をリファクタします。
IDateTime
を実装したクラスをコンストラクターの引数を通じて受け取るだけです。
public class GreetingService
{
private readonly IDateTime _dateTime;
public GreetingService(IDateTime dateTime)
=> _dateTime = dateTime;
public string Greet(string name)
{
var time = _dateTime.Now.TimeOfDay;
if (new TimeSpan(5, 00, 00) <= time && time < new TimeSpan(10, 00, 00))
return $"おはよう!{name}さん!";
else if (new TimeSpan(10,00,00) <= time && time < new TimeSpan(17, 00, 00))
return $"こんにちは!{name}さん!";
else
return $"こんばんは!{name}さん!";
}
}
インターフェースを使用したことで、実装を簡単に差し替えることができます。
IDateTime
を実装したクラスなら DB 時刻を返す DatabaseDateTime
でもなんでも DI できます。
後述する ASP.NET Core のビルトイン DI コンテナーやモックライブラリ Moq
もインターフェースを使用します。
ユニットテストする!
xUnit でテストを行いました。
Moq
でモックを用意しましたが、IDateTime
が単純なのでフェイククラスを作成してもOKかと。
public class GreetingServiceTest
{
[Fact]
public void Returns_valid_string_at_0500()
{
// Arrange
var dateTime = new Mock<IDateTime>();
dateTime.Setup(d => d.Now)
.Returns(DateTime.Parse("2018-04-14 05:00:00"));
var sut = new GreetingService(dateTime.Object);
var name = "John Doe";
// Act
var greeting = sut.Greet(name);
// Assert
Assert.Equal($"おはよう!{name}さん!", greeting);
}
}
時刻を簡単に変更してテストでき、幸せになれました
実際に使ってみる
DI できるようになったので ASP.NET Core のビルトイン DI コンテナーで使用してみます。
GreetingService
も DI できるようにインターフェースを実装し、 API の Controller クラスで利用します。
public interface IGreeting
{
string Greet(string name);
}
public class GreetingService : IGreeting
{
:
:
}
Startup.cs
の ConfigureServices
メソッドで DI コンテナに登録します。
public void ConfigureServices(IServiceCollection services) =>
services.AddTransient<IDateTime, ServerDateTime>()
.AddTransient<IGreeting, GreetingService>()
.AddMvc();
それでは実際に Web API で使ってみます。
[Route("api/[controller]")]
public class GreetingController : Controller
{
private readonly IGreeting _greetingServie;
public GreetingController(IGreeting greetingService) =>
_greetingServie = greetingService;
[HttpGet("{name}")]
public string Get(string name) =>
_greetingServie.Greet(name);
}
バッチリ動きました!