单元测试仅在构建服务器上 运行 时失败
Unit test failing only when run on the build server
为了帮助单元测试,我们将 DateTime
class 包裹在一个委托中,这样 DateTime.Now
就可以在单元测试中被覆盖。
public static class SystemTime
{
#region Static Fields
public static Func<DateTime> Now = () => DateTime.Now;
#endregion
}
这是它在 xunit 单元测试中的使用示例:
[Fact]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
// Arrange
var service = this.CreateClassUnderTest();
var expectedTimestamp = SystemTime.Now();
SystemTime.Now = () => expectedTimestamp;
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(expectedTimestamp, this._testEntry.LastAccessedOn);
}
测试 运行 在本地很好,但是由于 Assert
语句中的日期时间不同,它在我们的构建服务器上失败了。
鉴于 DateTime
是通过上述委托包装器模拟的,我正在努力思考它失败的原因。我已经验证 UpdateLastAccessedTimestamp
方法的实施没有问题,并且当本地 运行 时测试通过。
不幸的是,我无法在我们的构建服务器上调试它。为什么只有在构建服务器上 运行 时它才会失败?
注意UpdateLastAccessedTimestamp
的实现如下:
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
Entry
class 只是一个简单的 POCO class,它有许多字段,包括 LastAccessedOn
字段:
public class Entry
{
public DateTime LastAccessedOn { get; set; }
//other fields that have left out to keep the example concise
}
你很幸运它能在本地运行。为了使这项工作正常进行,您必须对服务获取其最后访问数据时间的位置进行存根,并检查那里的时间 return。此时你的本地机器足够快 return 在 DataTime.Now 上同时运行 2 次而你的构建服务器不是。
您的问题可能是由于 static SystemTime
的多个单元测试造成的。例如,如果你有类似的东西:
单元测试 1
[Fact]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
// Arrange
var service = this.CreateClassUnderTest();
var expectedTimestamp = SystemTime.Now();
SystemTime.Now = () => expectedTimestamp;
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(expectedTimestamp, this._testEntry.LastAccessedOn);
}
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
单元测试 2
[Fact]
public void do_something_different
{
SystemTime.Now = () => DateTime.Now;
}
所以让我们假设单元测试 2(它是完整的)在单元测试 1 的行之间触发:
SystemTime.Now = () => expectedTimestamp;
// Unit test 2 starts execution here
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
如果发生这种情况,那么您的 UpdateLastAccessedTimestamp
将不会(必然)具有您设置为 SystemTime.Now = () => expectedTimestamp;
的预期 DateTime
值,因为另一个测试已经覆盖了您在单元测试 1 中提供的功能。
这就是为什么我认为您最好将 DateTime
作为参数传递,或者像这样使用可注入的日期时间:
/// <summary>
/// Injectable DateTime interface, should be used to ensure date specific logic is more testable
/// </summary>
public interface IDateTime
{
/// <summary>
/// Current Data time
/// </summary>
DateTime Now { get; }
}
/// <summary>
/// DateTime.Now - use as concrete implementation
/// </summary>
public class SystemDateTime : IDateTime
{
/// <summary>
/// DateTime.Now
/// </summary>
public DateTime Now { get { return DateTime.Now; } }
}
/// <summary>
/// DateTime - used to unit testing functionality around DateTime.Now (externalizes dependency on DateTime.Now
/// </summary>
public class MockSystemDateTime : IDateTime
{
private readonly DateTime MockedDateTime;
/// <summary>
/// Take in mocked DateTime for use in testing
/// </summary>
/// <param name="mockedDateTime"></param>
public MockSystemDateTime(DateTime mockedDateTime)
{
this.MockedDateTime = mockedDateTime;
}
/// <summary>
/// DateTime passed from constructor
/// </summary>
public DateTime Now { get { return MockedDateTime; } }
}
使用这种情况,您的服务 class 可能会从(类似于)这样改变:
public class Service
{
public Service() { }
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
}
为此:
public class Service
{
private readonly IDateTime _iDateTime;
public Service(IDateTime iDateTime)
{
if (iDateTime == null)
throw new ArgumentNullException(nameof(iDateTime));
// or you could new up the concrete implementation of SystemDateTime if not provided
_iDateTime = iDateTime;
}
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = _iDateTime.Now;
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
}
对于 Service
的实际实施,您可以像这样(或使用 IOC 容器):
Service service = new Service(new SystemDateTime());
对于测试,您可以使用模拟框架,或者您的 Mock class:
Service service = new Service(new MockDateTime(new DateTime(2000, 1, 1)));
你的单元测试可能会变成:
[Fact]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
// Arrange
MockDateTime mockDateTime = new MockDateTime(new DateTime 2000, 1, 1);
var service = this.CreateClassUnderTest(mockDateTime);
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(mockDateTime.Now, this._testEntry.LastAccessedOn);
}
为了帮助单元测试,我们将 DateTime
class 包裹在一个委托中,这样 DateTime.Now
就可以在单元测试中被覆盖。
public static class SystemTime
{
#region Static Fields
public static Func<DateTime> Now = () => DateTime.Now;
#endregion
}
这是它在 xunit 单元测试中的使用示例:
[Fact]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
// Arrange
var service = this.CreateClassUnderTest();
var expectedTimestamp = SystemTime.Now();
SystemTime.Now = () => expectedTimestamp;
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(expectedTimestamp, this._testEntry.LastAccessedOn);
}
测试 运行 在本地很好,但是由于 Assert
语句中的日期时间不同,它在我们的构建服务器上失败了。
鉴于 DateTime
是通过上述委托包装器模拟的,我正在努力思考它失败的原因。我已经验证 UpdateLastAccessedTimestamp
方法的实施没有问题,并且当本地 运行 时测试通过。
不幸的是,我无法在我们的构建服务器上调试它。为什么只有在构建服务器上 运行 时它才会失败?
注意UpdateLastAccessedTimestamp
的实现如下:
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
Entry
class 只是一个简单的 POCO class,它有许多字段,包括 LastAccessedOn
字段:
public class Entry
{
public DateTime LastAccessedOn { get; set; }
//other fields that have left out to keep the example concise
}
你很幸运它能在本地运行。为了使这项工作正常进行,您必须对服务获取其最后访问数据时间的位置进行存根,并检查那里的时间 return。此时你的本地机器足够快 return 在 DataTime.Now 上同时运行 2 次而你的构建服务器不是。
您的问题可能是由于 static SystemTime
的多个单元测试造成的。例如,如果你有类似的东西:
单元测试 1
[Fact]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
// Arrange
var service = this.CreateClassUnderTest();
var expectedTimestamp = SystemTime.Now();
SystemTime.Now = () => expectedTimestamp;
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(expectedTimestamp, this._testEntry.LastAccessedOn);
}
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
单元测试 2
[Fact]
public void do_something_different
{
SystemTime.Now = () => DateTime.Now;
}
所以让我们假设单元测试 2(它是完整的)在单元测试 1 的行之间触发:
SystemTime.Now = () => expectedTimestamp;
// Unit test 2 starts execution here
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
如果发生这种情况,那么您的 UpdateLastAccessedTimestamp
将不会(必然)具有您设置为 SystemTime.Now = () => expectedTimestamp;
的预期 DateTime
值,因为另一个测试已经覆盖了您在单元测试 1 中提供的功能。
这就是为什么我认为您最好将 DateTime
作为参数传递,或者像这样使用可注入的日期时间:
/// <summary>
/// Injectable DateTime interface, should be used to ensure date specific logic is more testable
/// </summary>
public interface IDateTime
{
/// <summary>
/// Current Data time
/// </summary>
DateTime Now { get; }
}
/// <summary>
/// DateTime.Now - use as concrete implementation
/// </summary>
public class SystemDateTime : IDateTime
{
/// <summary>
/// DateTime.Now
/// </summary>
public DateTime Now { get { return DateTime.Now; } }
}
/// <summary>
/// DateTime - used to unit testing functionality around DateTime.Now (externalizes dependency on DateTime.Now
/// </summary>
public class MockSystemDateTime : IDateTime
{
private readonly DateTime MockedDateTime;
/// <summary>
/// Take in mocked DateTime for use in testing
/// </summary>
/// <param name="mockedDateTime"></param>
public MockSystemDateTime(DateTime mockedDateTime)
{
this.MockedDateTime = mockedDateTime;
}
/// <summary>
/// DateTime passed from constructor
/// </summary>
public DateTime Now { get { return MockedDateTime; } }
}
使用这种情况,您的服务 class 可能会从(类似于)这样改变:
public class Service
{
public Service() { }
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
}
为此:
public class Service
{
private readonly IDateTime _iDateTime;
public Service(IDateTime iDateTime)
{
if (iDateTime == null)
throw new ArgumentNullException(nameof(iDateTime));
// or you could new up the concrete implementation of SystemDateTime if not provided
_iDateTime = iDateTime;
}
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = _iDateTime.Now;
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
}
对于 Service
的实际实施,您可以像这样(或使用 IOC 容器):
Service service = new Service(new SystemDateTime());
对于测试,您可以使用模拟框架,或者您的 Mock class:
Service service = new Service(new MockDateTime(new DateTime(2000, 1, 1)));
你的单元测试可能会变成:
[Fact]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
// Arrange
MockDateTime mockDateTime = new MockDateTime(new DateTime 2000, 1, 1);
var service = this.CreateClassUnderTest(mockDateTime);
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(mockDateTime.Now, this._testEntry.LastAccessedOn);
}