【问题标题】:Unit test failing only when run on the build server单元测试仅在构建服务器上运行时失败
【发布时间】:2016-03-25 20:47:31
【问题描述】:

为了帮助进行单元测试,我们将DateTime 类封装在一个委托中,以便可以在单元测试中覆盖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 类只是一个简单的 POCO 类,它有许多字段,包括 LastAccessedOn 字段:

public class Entry
{
   public DateTime LastAccessedOn { get; set; }

   //other fields that have left out to keep the example concise
}

【问题讨论】:

  • UpdateLastAccessedTimestamp 是否进行任何日期操作,或者可能从外部来源获取时间?你能看到expected和actual之间的失败原因(values)吗? this._testEntry 是什么?您的 Act 部分似乎没有传入您的预期时间戳
  • @Kritner 感谢您的回复。我已根据您的反馈更新了我的问题。请注意,对 _unitOfWork 对象的调用不会对 LastAccessedOn 字段进行任何操作。
  • 只是好奇...如果你用SystemTime.Now = () =&gt; expectedTimestamp; 代替SystemTime.Now = () =&gt; new DateTime(2000, 1, 1); - 你的结果会是什么?您的预期是 2000、1、1 吗?你的实际是 2000、1、1 吗?还是可能是 2016 年 2 月 24 日?希望这至少有助于缩小问题所在。
  • 你能分享CreateClassUnderTest和_testEntry吗?

标签: c# unit-testing datetime nunit xunit


【解决方案1】:

您的问题可能是由于使用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 = () =&gt; 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; } }
}

使用这种情况,您的服务类可能会从(类似)这样改变:

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 类:

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);
}   

【讨论】:

  • 非常感谢您的详细解释。
  • 没问题 - 你能确认这是多个​​单元测试覆盖了SystemTime 提供的实现吗?这只是一个最好的猜测:P
【解决方案2】:

你很幸运它在本地工作。要使这项工作正常工作,您必须对服务获取其最后访问数据时间的地方进行存根,并检查那里的时间返回。此时,您的本地计算机足够快,可以在 DataTime 上同时返回 2 次。现在您的构建服务器不是。

【讨论】:

  • 感谢您的回复。 DateTime 通过服务中的委托访问。我已经更新了我的问题以包括 UpdateLastAccessedTimestamp 方法的实现。
猜你喜欢
  • 2021-03-28
  • 2011-10-05
  • 2021-11-10
  • 2011-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-21
  • 2013-10-13
相关资源
最近更新 更多