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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.