本文介绍了单元测试仅在构建服务器上运行时失败的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!
问题描述
为了帮助单元测试,我们在委托中包装了 DateTime
类,以便 DateTime.Now
可以在单位测试中被覆盖。
public static class SystemTime
{
#region静态字段
public static Func< DateTime> Now =()=> DateTime.Now;
#endregion
}
这是一个例子使用xunit单元测试:
[事实]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
//安排
var service = this.CreateClassUnderTest();
var expectedTimestamp = SystemTime.Now();
SystemTime.Now =()=>预期Timestamp;
// 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();
}
条目
只是一个简单的POCO类,它有一些字段,包括 LastAccessedOn
字段:
public class Entry
{
public DateTime LastAccessedOn {get;组; $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $
$ b div class =h2_lin>解决方案
您的问题可能是由于使用静态SystemTime
的多个单元测试。如果你有例如:
单元测试1
[Fact]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
//排列
var service = this.CreateClassUnderTest();
var expectedTimestamp = SystemTime.Now();
SystemTime.Now =()=>预期Timestamp;
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(expectedTimestamp,this._testEntry.LastAccessedOn);
}
public void UpdateLastAccessedTimestamp(条目条目)
{
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 =()=>预期Timestamp;
//单元测试2在这里开始执行
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
如果发生这种情况,那么您的 UpdateLastAccessedTimestamp
不会(必然)在 SystemTime.Now =()=>中设置您的预期 DateTime
expectedTimestamp;
,因为另一个测试已经覆盖了您从单元测试1提供的功能。
这就是为什么我认为你可能更好地将 DateTime
作为参数传递,或者使用可注入的日期时间:
///< summary>
///可注入DateTime接口,应该用来确保日期特定的逻辑更可测试
///< / summary>
public interface IDateTime
{
///< summary>
///当前数据时间
///< / summary>
DateTime Now {get; }
}
///< summary>
/// DateTime.Now - 用作具体实现
///< / summary>
public class SystemDateTime:IDateTime
{
///< summary>
/// DateTime.Now
///< / summary>
public DateTime Now {get {return DateTime.Now; }}
}
///< summary>
/// DateTime - 用于围绕DateTime.Now进行单元测试功能(外部依赖于DateTime.Now
///< / summary>
public class MockSystemDateTime:IDateTime
{
private readonly DateTime MockedDateTime;
///< summary>
///使用嘲弄的DateTime用于测试
///< /摘要>
///< param name =mockedDateTime>< / param>
public MockSystemDateTime(DateTime mockedDateTime)
{
this.MockedDateTime = mockedDateTime;
}
///< summary>
///从构造函数传递的DateTime
///< / summary>
public DateTime Now {获取{return MockedDateTime;}}
}
使用这种情况,您的服务类可能会更改从(这样的)这个:
public class Service
{
public Service(){}
public void UpdateLastAccessedTimesta mp(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));
//或者您可以新增SystemDateTime的具体实现(如果不提供)
_iDateTime = iDateTime;
}
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = _iDateTime.Now;
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
}
为了实际执行您的服务
你可能会喜欢(或使用IOC容器):
服务服务=新服务(新的SystemDateTime());
对于测试,您可以使用嘲笑框架或Mock类:
服务服务=新服务(新的MockDateTime(新的DateTime(2000,1,1)));
您的单元测试可能会变成:
[事实]
public void it_should_update_the_last_accessed_timestamp_on_an_entry()
{
//排列
MockDateTime mockDateTime = new MockDateTime(new DateTime 2000, 1);
var service = this.CreateClassUnderTest(mockDateTime);
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
// Assert
Assert.Equal(mockDateTime.Now,this._testEntry.LastAccessedOn);
}
To aid unit-testing we have wrapped up the DateTime
class in a delegate so that DateTime.Now
can be overridden in a unit-test.
public static class SystemTime
{
#region Static Fields
public static Func<DateTime> Now = () => DateTime.Now;
#endregion
}
Here is an example of its use in an xunit unit-test:
[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);
}
The test runs fine locally, however it is failing on our build server as the datetimes differ in the Assert
statement.
I'm struggling to think of a reason why it would fail given that the DateTime
is mocked via the above mentioned delegate wrapper. I've verified there are no issues in the implementation of the UpdateLastAccessedTimestamp
method and that the test passes when run locally.
Unfortunately I can't debug it on our build server. Any ideas why it would fail only when run on the build server?
Note that the implementation of UpdateLastAccessedTimestamp
is as follows:
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
The Entry
class is just a simple POCO class that has a number of fields including the LastAccessedOn
field:
public class Entry
{
public DateTime LastAccessedOn { get; set; }
//other fields that have left out to keep the example concise
}
解决方案
Your issue could be due to multiple unit tests working with the static SystemTime
. If you were to for example have something like:
Unit test 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();
}
Unit test 2
[Fact]
public void do_something_different
{
SystemTime.Now = () => DateTime.Now;
}
So let's assume that unit test 2 (it's entirety) fires off in between the lines in unit test 1 of:
SystemTime.Now = () => expectedTimestamp;
// Unit test 2 starts execution here
// Act
service.UpdateLastAccessedTimestamp(this._testEntry);
If this scenario were to occur, then your UpdateLastAccessedTimestamp
would not (necessarily) have your expected DateTime
value that you set at SystemTime.Now = () => expectedTimestamp;
, as another test has since overwritten the function you have provided from unit test 1.
This is why I think you're probably better off either passing the DateTime
in as a parameter, or using an injectable datetime as so:
/// <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; } }
}
Using this scenario, your service class could change from (something like) this:
public class Service
{
public Service() { }
public void UpdateLastAccessedTimestamp(Entry entry)
{
entry.LastAccessedOn = SystemTime.Now();
this._unitOfWork.Entries.Update(entry);
this._unitOfWork.Save();
}
}
To this:
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();
}
}
For the actual implementation of your Service
you could new up like (or use an IOC container):
Service service = new Service(new SystemDateTime());
For testing you could either use a mocking framework, or your Mock class as such:
Service service = new Service(new MockDateTime(new DateTime(2000, 1, 1)));
And your unit test could become:
[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);
}
这篇关于单元测试仅在构建服务器上运行时失败的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!