Unit test failing only when run on the build serve

2019-07-01 10:33发布

问题:

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
}

回答1:

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


回答2:

You are just lucky it works local. To get this working you have to stub the place where your service gets its last access datatime and check against the time return there. At this moment you local machine is fast enough to return 2 times the same time on DataTime.Now and your build server is not.