Skip to content

Commit 0eafd9e

Browse files
chore: make Clock static. (#8)
1 parent da02c85 commit 0eafd9e

7 files changed

Lines changed: 115 additions & 27 deletions

File tree

README.md

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,70 @@
11
# Timecop - Easy Date and Time Testing in C\#
22

3-
Timecop is a small library that helps you test DateTime in a static, thread-safe, ambient context.
3+
Timecop is a small library that helps to test the code that depends on DateTime by allowing you to freeze and travel in time.
44

55
Timecop targets .NET Standard 2.0, has no external dependencies, and can be used with .NET Framework 4.5+ and any version of .NET and .NET Core.
66

77
Timecop has been inspired by the [timecop](https://github.com/travisjeffery/timecop) Ruby gem.
88

99
## Installation
1010

11-
You can install [Timecop](https://www.nuget.org/packages/Timecop/) from NuGet using the .NET CLI:
11+
You can install [Timecop](https://www.nuget.org/packages/Timecop/) from NuGet using your IDE or the .NET CLI:
1212

1313
```
1414
dotnet add package Timecop
1515
```
1616

17-
## Basic usage
17+
## Basics and usage
1818

19-
Timecop allows you to freeze and travel in time. Just use the `Clock` class instead of `DateTime`to get the current time via `Now` or `UtcNow` properties, and manipulate time with the `Timecop` class in your tests.
19+
To test with Timecop, your code must get the current time using either:
20+
- The `IClock` interface injected as a method or a constructor parameter;
21+
- The static `Clock` class.
22+
23+
### Usage with the `IClock` interface
24+
25+
Timecop provides the `IClock` interface that exposes the `Now` and `UtcNow` properties.
26+
27+
Here's how to use it:
28+
1. Pass the instance of `IClock` to your code as a method or a constructur parameter and use it to get the current time;
29+
1. In your tests, configure the `Timecop` instance and pass the `Timecop.Clock` into the code under test;
30+
1. When running in production, pass the `IClock` implementaton that returns the current time. You can use the [Timecop.Extensions.DependencyInjection](https://github.com/timecop-net/Timecop.Extensions.DependencyInjection) package to register such implementation with the DI container in one line of code.
31+
32+
Here's an example:
33+
34+
```csharp
35+
string Greet(IClock clock)
36+
{
37+
var timeOfDay = clock.Now.Hour switch
38+
{
39+
>= 0 and < 6 => "night",
40+
>= 6 and < 12 => "morning",
41+
>= 12 and < 18 => "afternoon",
42+
_ => "evening"
43+
};
44+
45+
return $"Good {timeOfDay}!";
46+
}
47+
48+
// freeze at 2pm local time:
49+
using var tc = Timecop.Frozen(o => o.At(14,0,0).LocalTime());
50+
51+
Greet(tc.Clock); // Good afternoon!
52+
53+
// travel to 8pm local time:
54+
tc.TravelBy(TimeSpan.FromHours(6));
55+
56+
Greet(tc.Clock); // Good evening!
57+
```
58+
59+
### Usage with the `Clock` class
60+
61+
Timecop provides the static `Clock` class that you can use instead of `DateTime` to get the current local or UTC time. Despite `Clock` being a static class, it is safe to use in tests that run in parallel as it uses [AsyncLocal](https://learn.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1) under the hood.
62+
63+
Here's how to use it:
64+
1. Replace the calls to `DateTime.Now` and `DateTime.UtcNow` with `Clock.Now` and `Clock.UtcNow`;
65+
1. In your tests, configure the `Timecop` instance
66+
67+
Example:
2068

2169
```csharp
2270
string Greet()
@@ -45,11 +93,13 @@ Greet(); // Good evening!
4593

4694
## Available methods
4795

96+
Timecop allows to manipulate time in any imaginable way. Freeze time, travel in time, and resume the flow of time with a simple API.
97+
4898
### Freezing and resuming time
4999

50-
You can freeze the time so that it stops running for your tests until you call `Resume` or dispose the `Timecop` instance.
100+
You can freeze the time so that it stops running for your tests until you call `Resume`, `Reset`, or dispose the `Timecop` instance.
51101

52-
You freeze time with either an instance `Freeze` or a static `Frozen` method, which both have the same set of overloads. Both methods have the same effect, however the static `Frozen` creates an already frozen `Timecop` instance.
102+
You freeze time with either an instance `Freeze` or a static `Frozen` method, both having the same set of overloads. Both methods have the same effect, however the static `Frozen` creates an already frozen `Timecop` instance.
53103

54104
```csharp
55105
using var tc = Timecop.Frozen(1990, 12, 2, 14, 38, 51, DateTimeKind.Local);
@@ -60,11 +110,11 @@ Thread.Sleep(TimeSpan.FromSeconds(3));
60110

61111
Clock.Now; // 1990-12-02 14:38:51 - still the same value
62112
63-
tc.Resume();
113+
tc.Resume(); // Resumes the flow of time.
64114
65115
Thread.Sleep(TimeSpan.FromSeconds(3));
66116

67-
Clock.Now; // 1990-12-02 14:38:54 - time has changed
117+
Clock.Now; // 1990-12-02 14:38:54 - moved 3 seconds forward
68118
```
69119

70120
`Freeze` and `Frozen` have multiple overloads:
@@ -73,24 +123,24 @@ Clock.Now; // 1990-12-02 14:38:54 - time has changed
73123
// freeze at the current instant:
74124
var frozenAt = tc.Freeze();
75125

76-
// freeze at the specified DateTime:
126+
// freeze at the specific DateTime:
77127
frozenAt = tc.Freeze(new DateTime(1990, 12, 2, 14, 38, 51, DateTimeKind.Utc));
78128

79-
// freeze at the specified date and time:
129+
// freeze at the specific date and time:
80130
frozenAt = tc.Freeze(1990, 12, 2, 14, 38, 51, DateTimeKind.Utc);
81131

82-
// freeze at the specified date:
132+
// freeze at the specific date:
83133
frozenAt = tc.Freeze(1990, 12, 2, DateTimeKind.Utc);
84134

85-
// freeze at the specified date or time using a builder:
135+
// freeze at the specific date or time using PointInTimeBuilder:
86136
frozenAt = tc.Freeze(o => o.On(1990, 12, 2)
87137
.At(14, 13, 51)
88-
.LocalTime());
138+
.InLocalZone());
89139
```
90140

91141
### Traveling in time
92142

93-
Use the `TravelBy` method to travel forward and backward in time:
143+
Use the `TravelBy` method to travel forward and backward in time:
94144

95145
```csharp
96146
using var tc = Timecop.Frozen(1990, 12, 2, 14, 38, 51, DateTimeKind.Local);
@@ -100,6 +150,44 @@ tc.TravelBy(TimeSpan.FromDays(1));
100150
Clock.Now; // 1990-12-03 14:38:51 - one day in the future
101151
```
102152

153+
Use the `TravelTo` method to travel to the specific point in time:
154+
155+
```csharp
156+
// travel to the specific DateTime:
157+
traveledTo = tc.TravelTo(new DateTime(1990, 12, 2, 14, 38, 51, DateTimeKind.Utc));
158+
159+
// freeze at the specific date and time:
160+
traveledTo = tc.TravelTo(1990, 12, 2, 14, 38, 51, DateTimeKind.Utc);
161+
162+
// freeze at the specific date:
163+
traveledTo = tc.TravelTo(1990, 12, 2, DateTimeKind.Utc);
164+
165+
// freeze at the specific date or time using PointInTimeBuilder:
166+
traveledTo = tc.TravelTo(o => o.On(1990, 12, 2)
167+
.At(14, 13, 51)
168+
.InLocalZone());
169+
```
170+
171+
### Using PointInTimeBuilder
172+
173+
`Freeze`, `Frozen`, and `TravelTo` methods each accept a lambda that allows to configure the time with the `PointInTimeBuilder` class.
174+
175+
Use `PointInTimeBuilder` to specify different components of date and time. When using `At` and `On` methods, always specify whether the time is local or UTC using `InLocalZone` or `InUtc`.
176+
177+
178+
```csharp
179+
// When only the date matters:
180+
builder.On(1990, 12, 2).InLocalZone(); // will use the specified date and current time
181+
182+
// When only the time matters:
183+
builder.At(14, 13, 51).InLocalZone(); // will use the specified time and current date
184+
185+
// When neither the date nor time matter, but the date must be in the future or in the past:
186+
builder.InTheFuture();
187+
188+
builder.InThePast();
189+
```
190+
103191
## License
104192

105193
Timecop was created by [Dmytro Khmara](https://dmytrokhmara.com) and is licensed under the [MIT license](LICENSE.txt).

src/Timecop/Clock.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace TCop;
44

5-
public class Clock
5+
public static class Clock
66
{
77
/// <summary>Returns either current or pre-configured local time. Time can be pre-configured by using <see cref="T:TCop.Timecop" />.</summary>
88
public static DateTime Now => Timecop.UtcNow.ToLocalTime();

src/Timecop/Time/Builder/PointInTimeBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ public PointInTimeBuilder On(int year, int month, int day)
1919
return this;
2020
}
2121

22-
public PointInTimeBuilder LocalTime()
22+
public PointInTimeBuilder InLocalZone()
2323
{
2424
_context.Kind = DateTimeKind.Local;
2525
return this;
2626
}
2727

28-
public PointInTimeBuilder UtcTime()
28+
public PointInTimeBuilder InUtc()
2929
{
3030
_context.Kind = DateTimeKind.Utc;
3131
return this;

src/Timecop/Time/Builder/PointInTimeBuilderNeitherLocalNorUtcException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace TCop.Time.Builder;
44

55
public class PointInTimeBuilderNeitherLocalNorUtcException : Exception
66
{
7-
public PointInTimeBuilderNeitherLocalNorUtcException() : base($"Call either {nameof(PointInTimeBuilder.LocalTime)}() or {nameof(PointInTimeBuilder.UtcTime)}() when configuring the point in time.")
7+
public PointInTimeBuilderNeitherLocalNorUtcException() : base($"Call either {nameof(PointInTimeBuilder.InLocalZone)}() or {nameof(PointInTimeBuilder.InUtc)}() when configuring the point in time.")
88
{
99
}
1010
}

test/Timecop.Tests/Time/PointInTimeBuilderTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class PointInTimeBuilderTests
1212
[Fact]
1313
public void LocalTime_ShouldReturnCurrentLocalTime()
1414
{
15-
_builder.LocalTime();
15+
_builder.InLocalZone();
1616

1717
_builder.Build(out var kind).DateTimeUtc.ToLocalTime().Should().BeCloseTo(DateTime.Now, DateTimeComparisonPrecision);
1818
kind.Should().Be(DateTimeKind.Local);
@@ -21,7 +21,7 @@ public void LocalTime_ShouldReturnCurrentLocalTime()
2121
[Fact]
2222
public void UtcTime_ShouldReturnCurrentUtcTime()
2323
{
24-
_builder.UtcTime();
24+
_builder.InUtc();
2525

2626
_builder.Build(out var kind).DateTimeUtc.Should().BeCloseTo(DateTime.UtcNow, DateTimeComparisonPrecision);
2727
kind.Should().Be(DateTimeKind.Utc);
@@ -34,7 +34,7 @@ public void Build_OnWasCalled_ButNeitherLocalNorUtcWasCalled_ShouldThrow()
3434

3535
var build = () => _builder.Build(out _);
3636

37-
build.Should().Throw<PointInTimeBuilderNeitherLocalNorUtcException>().WithMessage("Call either LocalTime() or UtcTime() when configuring the point in time.");
37+
build.Should().Throw<PointInTimeBuilderNeitherLocalNorUtcException>().WithMessage("Call either InLocalZone() or InUtc() when configuring the point in time.");
3838
}
3939

4040
[Fact]
@@ -44,7 +44,7 @@ public void Build_AtWasCalled_ButNeitherLocalNorUtcWasCalled_ShouldThrow()
4444

4545
var build = () => _builder.Build(out _);
4646

47-
build.Should().Throw<PointInTimeBuilderNeitherLocalNorUtcException>().WithMessage("Call either LocalTime() or UtcTime() when configuring the point in time.");
47+
build.Should().Throw<PointInTimeBuilderNeitherLocalNorUtcException>().WithMessage("Call either InLocalZone() or InUtc() when configuring the point in time.");
4848
}
4949

5050
[Fact]
@@ -59,7 +59,7 @@ public void InTheFuture_ShouldReturnPointOfTimeInTheFuture()
5959
[Fact]
6060
public void InTheFuture_WithLocalTime_ShouldReturnPointOfTimeInTheFutureInLocalTime()
6161
{
62-
_builder.InTheFuture().LocalTime();
62+
_builder.InTheFuture().InLocalZone();
6363

6464
_builder.Build(out var kind).DateTimeUtc.Should().BeAfter(DateTime.UtcNow);
6565
kind.Should().Be(DateTimeKind.Local);
@@ -79,7 +79,7 @@ public void On_ShouldReturnSetDateAndCurrentTime()
7979
{
8080
_builder
8181
.On(1990, 12, 2)
82-
.LocalTime();
82+
.InLocalZone();
8383

8484
var now = DateTime.Now;
8585

@@ -93,7 +93,7 @@ public void At_ShouldReturnSetTimeAndCurrentDate()
9393
{
9494
_builder
9595
.At(14, 15, 30, 893)
96-
.LocalTime();
96+
.InLocalZone();
9797

9898
var now = DateTime.Now;
9999

test/Timecop.Tests/TimecopFreezeTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public void FreezeAtRandomFutureLocalTime_ShouldFreeze_AndReturnFrozenLocalDateT
6666
{
6767
using var tc = new Timecop();
6868

69-
var frozenAt = tc.Freeze(o => o.InTheFuture().LocalTime());
69+
var frozenAt = tc.Freeze(o => o.InTheFuture().InLocalZone());
7070
var currentLocalTime = DateTime.Now;
7171

7272
Thread.Sleep(100);

test/Timecop.Tests/TimecopTravelTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void TravelToRandomFutureLocalTime_ShouldTravel_AndReturnTheLocalDateTime
7070
{
7171
using var tc = new Timecop();
7272

73-
var traveledTo = tc.TravelTo(o => o.InTheFuture().LocalTime());
73+
var traveledTo = tc.TravelTo(o => o.InTheFuture().InLocalZone());
7474
var currentLocalTime = DateTime.Now;
7575

7676
Thread.Sleep(100);

0 commit comments

Comments
 (0)