C# TimeProvider 사용 정보(.NET8)

반응형

C#에는 시간을 표현하는 클래스로 DateTime 와 DateTimeOffset 가 있다.

.NET 8부터 TimeProvider 클래스가 새로 준비 되었다.

 

TimeProvider클래스는 .NET8 의 새로운 기능의 하나로「시간 추상화 (Time abstraction)」로서 소개되고 있다. 시간 추상화는 코드 테스트에 새로운 이점이 있다. 그 내용을 기록한 글이다.

DateTime.Now 와의 차이

TimeProvider 클래스는 현재 시간을 얻을 수 있다. 기존 DateTime 클래스를 사용하여 현재 시간을 얻은 경우와 비교한다.

 

public void Run(string[] args)
{
   
var now = TimeProvider.System.GetLocalNow();
   
var utcNow = TimeProvider.System.GetUtcNow();

   Console.WriteLine(now);
   Console.WriteLine(utcNow);

   
var now2 = DateTime.Now;
   
var utcNow2 = DateTimeOffset.UtcNow;

   Console.WriteLine(now2);
   Console.WriteLine(utcNow2);
}

 

2023/12/08 15:53:10 +09:00

2023/12/08 6:53:10 +00:00

2023/12/08 15:53:10

2023/12/08 6:53:10 +00:00

 

이것만이라면 차이는 없을 것 같다. 테스트 코드에서 DateTime.Now 를 호출하는 경우와 TimeProvider.System.GetLocalNow() 를 호출하는 경우라면, 어떤 차이가 나오는지가 앞서 읽은 포인트가 된다.

 

덧붙여서 DateTimeProvider 로부터 취득한 변수는 now utcNow는 모두 DateTimeOffset 타입이다. 그래도 GetLocalNow() 에서도 +09:00 이 출력되고 있다.

 

보충: DateTime 타입은 Kind 속성에서 UTC인지 Local인지 확인할 수 있다.

 

구현 예

(현지 시간) 정오(12시)인지를 체크하는 서비스 클래스를 구현해 보겠다.

 

시간 체크를 목적으로 한 클래스 중에서 시간을 준비하고 결과를 반환하도록 한다.

 

TimeProvider를 준비하는 부분은 어딘가에서 필요하지만 대부분의 시스템이라면 TimeProvider.System 이 좋다. 이것은 사양 책정 시에도 코멘트가 있었던 것 같다:

 

At the end of the day, we expect almost no one will use anything other than TimeProvider.System in production usage. Unlike many abstractions then, this one is special: it exists purely for testability.

 

public class TimeService
{
        
private readonly TimeProvider _TimeProvider;

        
public TimeService(TimeProvider timeProvider) => _TimeProvider = timeProvider;

        
public bool IsNoon()
        {
                
var now = _TimeProvider.GetLocalNow();

                
return now.Hour == 12;
        }
}

 

DateTime를 이용해도 유사한 로직은 가능하다. 이것이 테스트 중에 문제가 될까?

        

public bool IsNoon()
        {
                
var now = DateTime.Now;

                
return now.Hour == 12;
        }

 

테스트에 사용

가장 중요한 부분이다.

public class Tests
{
        
public class NoonTimeProvider : TimeProvider
        {
                
private readonly TimeSpan JST = new TimeSpan(9, 0, 0);

                
public override DateTimeOffset GetUtcNow()
                {
                        
return new DateTimeOffset(2023, 12, 1, 3, 0, 0, JST);
                }
        }

        [
SetUp]
        
public void Setup()
        {
        }

        [
Test]
        
public void Test()
        {
                
var testTimeProvider = new NoonTimeProvider();

                
var testService = new TimeService(testTimeProvider);
                
var isNoon = testService.IsNoon();

                Assert.IsTrue(isNoon);
        }
}

 

GetUtcNow을 override 하는 것으로 테스트 용 시간을 준비하고 있다. 테스트를 위해 추상화하는 클래스의 부분은 조금 부피가 커질 수 있다.

 

만약 TimeService 클래스에서 시간을 취득할 때를 DateTime.Now 을 이용했다면 어떨요? 위의 코드 예에서는 TimeProvider 클래스를 상속하여 GetUtcNow 메소드를 override 했지만,  DateTime 클래스는 seal 되어 있으므로 상속이 어렵고, 또 Now 프롭퍼티를 override 하는 것도 어렵고, 만일 어떻게든 할 수 있었다고 해도 까다로운 구현이 된다.

 

기본적으로 DateTime 나 DateTimeOffset은 (본래)어느 시간을 표현한 데이터 타입이다.  DateTime 이나 DateTimeOffset 에서 새롭게 현재 시간을 문의하는 Now 등의 기능은 편리하지만 확장하기 어렵다.

 

테스트 코드를 작성할 때 어려움이 있었기 때문에 이번에 TimeProvider 이 나왔다. 주의점으로서 TimeProvider 클래스의 GetLocalNow 는 override 할 수 없다. 그래서, 지금까지의 테스트 코드는 (다음에 설명을 한다) 형편이 나쁜 부분이 남아 있다. 한 단계 더 확장한다.

 

지속적인 통합 고려

IsNoon메서드는 메서드 내에서 GetLocalNow 을 사용하기 때문에 UTC 현지 시간 보정이 추가/감산된다. (일본이라면 +9:00과 같은 것) 그래서 (여기까지의 코드라면, 테스트 코드 실행자가 복수이고, 복수의 나라에 거점이 있는 경우) 테스트의 실행 환경에 의해 성공하거나 실패해 하는 우려가 있다.

 

이에 대응하는 코드는 아래와 같다.

 

public class Tests
{
        
public class NoonTimeProvider : TimeProvider
        {
                
public override DateTimeOffset GetUtcNow()
                {
                        
return new DateTimeOffset(2023, 12, 1, 3, 0, 0, TimeSpan.Zero);
                }

                
public override TimeZoneInfo LocalTimeZone =>
                        TimeZoneInfo.FindSystemTimeZoneById(
"Tokyo Standard Time");
        }

        [
SetUp]
        
public void Setup()
        {
        }

        [
Test]
        
public void Test()
        {
                
var testTimeProvider = new NoonTimeProvider();

                
var testService = new TimeService(testTimeProvider);
                
var isNoon = testService.IsNoon();

                Assert.IsTrue(isNoon);
        }
}

 

TimeProvider 시간대를 변경하고, 테스트 코드 중에 고정할 수 있다. FindSystemTimeZoneById 에서 시간대의 ID를 지정해야 한다.

 

List of Timezone IDs for use with FindTimeZoneById

 

위의 예에서는 테스트 코드를 도쿄의 UTC 시간으로 설정한다. 이제 테스트 실행 환경 차이를 추상화할 수 있다. 「시간의 추상화」라고 하는 TimeProvider클래스의 기능을 활용할 수 있었다, 라고 하는 이야기라고 생각한다.

 

여기까지의 이야기로부터 DateTime 이나 DateTimeOffset 에서 시간의 추상화 를 하려고 하면, 보다 테스트용의 모의가 비대화하는 것을 예상할 수 있다고 생각한다. (현지 시간만 있으면 좋을지도 모른다)

 

앞으로는 (시간에 관해서는) .NET8 TimeProvider 클래스를 활용한 테스트용 모의를 만들도록 하는 편이 좋은 경우가 많을 것이다.

ITimer 인터페이스

ITimer 인터페이스는 다음과 같다.

 

public interface ITimer : IDisposable, IAsyncDisposable
{
   
bool Change(TimeSpan dueTime, TimeSpan period);
}

 

아래와 같은 샘플 코드 제시가 「.NET8 의 새로운 기능」에 있지만 , 어떤 것을 설명하고 있는지 몰랐다.

 

// Create a timer using a time provider.
ITimer timer = timeProvider.CreateTimer(
   callBack, state, delay, Timeout.InfiniteTimeSpan);

// Measure a period using the system time provider.
long providerTimestamp1 = TimeProvider.System.GetTimestamp();
long providerTimestamp2 = TimeProvider.System.GetTimestamp();

var period = GetElapsedTime(providerTimestamp1, providerTimestamp2);

 

ITimer를 이용하고 있는 예는, FakeTimeProvider 를 이용하는 것을 찾았다.  NUnit의 경우 조금 더 많은 노력이 필요할 것이다.

FakeTimeProvider

 

샘플은 다음과 같다.

 

CreateTimer 는 dueTime 후에 period 간격으로를 state 를 인수로 한 콜백을 발생시킨다. 한 번만 발생시키는 경우는 Timeout.InfiniteTimeSpan(주기적인 콜백 무효)를 설정하여 dueTime 을 실행시간으로 한다.

 

테스트의 내용이 1시간이나 2시간이라고 하는 장시간의 경과를 보지 않으면 모르는 경우, 시간을 추상화하는 것으로 시간을 진행시켜 테스트를 구현한다. 현실 시간의 경과를 기다리는 것이 귀찮고, 현실적이지 않다. FakeTimeProvider 는 Advance라고 하는 메소드로 시간을 진행시킬 수가 있다.

timeProvider.Advance(TimeSpan.FromHours(1));

 

이미지로서는 이런 느낌이라고 생각한다.

[Test]
public void TestITimer()
{
        
var testTimeProvider = new FakeTimeProvider();
        
var result = false;

        
var itimer = testTimeProvider.CreateTimer(
                _ =>
                {
                        
// state 은  null 이므로 _  하고 있다
                        
// 시간 경과 후에 하는 것
                        result = 
true;
                },
                state: 
null,
                dueTime: TimeSpan.FromSeconds(1000),
                period: Timeout.InfiniteTimeSpan);

        testTimeProvider.Advance(TimeSpan.FromSeconds(1000));

        Assert.True(result);
}

 

참고

TimeProvider and ITimer: Writing Unit Tests with Time in .NET 8 Preview 4

TimeProvider - New Date & Time Abstractions in .NET 8!

반응형