LoginSignup
24
20

More than 5 years have passed since last update.

現在日時もDI(依存性注入)してテスト可能にする

Last updated at Posted at 2018-04-15

DateTime を使用するコードをテスト可能なイイ感じにします。

テストができないコード

DateTime が挙動にかかわるとユニットテストがしにくくなります。
時刻に応じて挨拶を返すサービスクラスを例に考えてみます。

Services/GreetingService.cs
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}さん!";
    }
}

現在時刻に応じて正しく文字列が返るかテストがしたいけど、できない...:cry:
これでは幸せになれません...。

DI(依存性注入)できるようにリファクタリング

dependency injection(依存性注入)はとても大雑把に説明すると、依存するオブジェクトをクラスの外から注入することです。
外から注入することで、動的に動作を変えられるようになります。

今回の場合、 DateTimeGreetingService の外から変えられればいいわけですね。

まず、IDateTime インタフェースを定義します。
DI の詳細の記事ではないので省略しますが、実装を隠ぺいするためです。
GreetingService が具体的な実装ではなく、 IDateTime インタフェースのみに依存するようにします。

Interfaces/IDateTime.cs
public interface IDateTime
{
    DateTime Now { get; }
}

次に IDateTime を実装したクラスを作成します。
今回はサーバーシステムの現在時刻を返すクラスを作成します。

Services/ServerDateTime
public class ServerDateTime : IDateTime
{
    public DateTime Now => DateTime.Now;
}

最後に GreetingService をリファクタします。
IDateTime を実装したクラスをコンストラクターの引数を通じて受け取るだけです。

Services/GreetingService.cs
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 できます。:grinning:
後述する ASP.NET Core のビルトイン DI コンテナーやモックライブラリ Moq もインターフェースを使用します。

ユニットテストする!

xUnit でテストを行いました。
Moq でモックを用意しましたが、IDateTime が単純なのでフェイククラスを作成してもOKかと。

GreetingServiceTest.cs
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);
    }
}

時刻を簡単に変更してテストでき、幸せになれました:blush:

実際に使ってみる

DI できるようになったので ASP.NET Core のビルトイン DI コンテナーで使用してみます。
GreetingService も DI できるようにインターフェースを実装し、 API の Controller クラスで利用します。

Interfaces/IGreeting.cs
public interface IGreeting
{
    string Greet(string name);
}
Services/GreetingService.cs
public class GreetingService : IGreeting
{
    :
    :
}

Startup.csConfigureServices メソッドで DI コンテナに登録します。

Startup.cs
public void ConfigureServices(IServiceCollection services) => 
    services.AddTransient<IDateTime, ServerDateTime>()
        .AddTransient<IGreeting, GreetingService>()
        .AddMvc();

それでは実際に Web API で使ってみます。

GreetingController.cs
[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);
}

バッチリ動きました!:grin:

image.png

24
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
20