wtorek, 26 czerwca 2012

Testy dla DateTime.Now

Załóżmy, że mamy klasę, w której podajemy dowolną datę i w której jest metoda zwracająca różnicę w ilościach dni między tą datą, a dzisiejszą datą. W implementacji takiego zadania stosujemy statyczną właściwość DateTime.Now. To pole przy każdym wywołaniu ma inną wartość. Problem może pojawić się kiedy będziemy musieli przetestować metodę zwracającą ilość dni (GetDaysFromNow), która w różnych momentach zwraca inny wynik.
    public class NowDateTimeCalculator
    {
        public  DateTime dateTime { get; private set; }
        public NowDateTimeCalculator(DateTime dateTime)
        {
            this.dateTime = dateTime;
        }
 
        public int GetDaysFromNow()
        {
            var timeSpanDiff = DateTime.Now - dateTime;
            return (int)timeSpanDiff.TotalDays;
        }
    }
Mamy kilka sposobów, aby mieć możliwość przetestowania tej klasy. Pierwsza możliwość to zastosowanie pośredniej klasy (interfejsu), która mogłaby dostarczyć informacji o aktualnej dacie. Tworzymy interfejs zwracający datę (Now) oraz klasę implementującą to pole.
    public interface INowDateTime
    {
        DateTime Now { get; }
    }
    public class NowDateTime : INowDateTime
    {
        public DateTime Now
        {
            get { return DateTime.Now; }
        }
   }

Jeszcze będziemy musieli zmienić naszą klasę NowDateTimeCalculator. Parametr z interfejsem będziemy wprowadzać przez konstruktor. To nam uprości pisanie testów.
public class NowDateTimeCalculator
{
    public  DateTime dateTime { get; private set; }
    private INowDateTime nowDateTime;

    public NowDateTimeCalculator(
                               DateTime dateTime,
                               INowDateTime iNowDateTime)
    {
        this.dateTime = dateTime;
        nowDateTime = iNowDateTime;
    }
 
    public int DaysFromNow2()
    {
        DateTime now = nowDateTime.Now;
        var timeSpanDiff = now  - dateTime;
        return (int)timeSpanDiff.TotalDays;
    }
}
Wywołanie wygląda następująco:
var dateTimeToCheck= new DateTime(2012, 06, 26);

var calc = new NowDateTimeCalculator(
dateTimeToCheck, new NowDateTime());

var fromNow= calc.GetDaysFromNow();
W łatwy sposób będziemy mogli zamokować interfejs INowDateTime. Test może wyglądać następująco:
[Test]
public void Check_if_GetDaysFromNow_Work_With_Mock()
{
    DateTime date_26_06_2012 = new DateTime(2012, 06, 26);
    var nowDateTimeMock = new Mock<INowDateTime>();

    nowDateTimeMock
               .Setup(x => x.Now)
               .Returns(date_26_06_2012);

    var calcToday =
 new NowDateTimeCalculator(date_26_06_2012,
                           nowDateTimeMock.Object);
 
   Assert.AreEqual(0, calcToday.GetDaysFromNow());

    var calcTomorrow =
 new NowDateTimeCalculator(date_26_06_2012.AddDays(1),
                           nowDateTimeMock.Object);
 
   Assert.AreEqual(-1, calcTomorrow.GetDaysFromNow());

    var calcYesterday =
 new NowDateTimeCalculator(date_26_06_2012.AddDays(-1),
                           nowDateTimeMock.Object);

    Assert.AreEqual(1, calcYesterday.GetDaysFromNow());
}
Takie rozwiązanie jest łatwe w implementacji i w testowaniu, ale ma 2 ważne wady - tworzymy zależności od interfejsu INowDateTime oraz takie rozwiązanie musi być znane dla całego zespołu.


Innym rozwiązanie jest zastosowanie statycznej klasy.
internal static class SystemTime
{
    internal static Func<DateTime> 
                SetDateTimeNow = () => DateTime.Now;
 
    internal static DateTime Now
    {
        get { return SetDateTimeNow();}
    }
}


public class NowDateTimeCalculator
{
    public  DateTime dateTime { get; private set; }

    public NowDateTimeCalculator(DateTime dateTime)
    {
        this.dateTime = dateTime;
    }
 
    public int GetDaysFromNow()
    {
        DateTime now = SystemTime.Now;
        var timeSpanDiff = now  - dateTime;
        return (int)timeSpanDiff.TotalDays;
    }
}
Testy wyglądają następująco:
[Test]
public void Check_if_GetDaysFromNow_Work_With_SystemTime()
{
    DateTime date_26_06_2012 = new DateTime(2012, 06, 26);

    SystemTime.SetDateTimeNow =
               () => date_26_06_2012;
    var calcToday = 
        new NowDateTimeCalculator(date_26_06_2012);
    Assert.AreEqual(0, calcToday.GetDaysFromNow());


    SystemTime.SetDateTimeNow = 
              () => date_26_06_2012.AddDays(2);
    var calcTomorrow =
    new NowDateTimeCalculator(date_26_06_2012.AddDays(1));

    Assert.AreEqual(1, calcTomorrow.GetDaysFromNow());


    SystemTime.SetDateTimeNow = 
              () => date_26_06_2012.AddDays(-2);
    var calcYesterday =
    new NowDateTimeCalculator(date_26_06_2012.AddDays(-1));

    Assert.AreEqual(-1, calcYesterday.GetDaysFromNow());
}
Takie rozwiązanie pozwala na przyjemniejsze testowanie naszej klasy. Podoba mi się łatwa zmiana aktualnej daty poprzez wyrażenie lambda. 'Wadą' może być to, że korzystamy z globalnej i statycznej klasy, która musi być znana dla całego zespołu.


Jest jeszcze jedno rozwiązanie tego problemu. To rozwiązanie jest znane tylko dla osoby, która pisze testy. Zastosujemy porównywanie wartości uwzględniając przy tym błąd(margines) w wartościach. Do tego potrzebny nam będzie interfejs z informacją o marginesie.
    public interface IMarginComparer<T> 
    {
        T CompareMargin { get; }
    }
Dla naszych testów będziemy musieli zaimplementować dwie porównywające klasy. Porównamy wartości dla typu DateTime oraz int.
public class DateTimeComparer : 
             IComparer<DateTime>, 
             IMarginComparer<TimeSpan>
{
    public DateTimeComparer(TimeSpan marginOfDateTime)
    {
        this.CompareMargin = marginOfDateTime;
    }
 
    public int Compare(DateTime x, DateTime y)
    {
        var margin = x - y;
        if (margin <= CompareMargin)
        {
            return 0;
        }
        return new Comparer(CultureInfo.CurrentUICulture)
                                       .Compare(x, y);
    }
 
    public TimeSpan CompareMargin
    {
        get; private set; 
    }
}
 



public class IntComparer : 
             IComparer<int>, 
             IMarginComparer<int>
{
    public IntComparer(int margin)
    {
        CompareMargin = margin;
 
    }
    public int Compare(int x, int y)
    {
        var diff = x - y;
        if (diff <= CompareMargin)
        {
            return 0;
        }
        return new Comparer(CultureInfo.CurrentUICulture)
                                       .Compare(x, y);
    }
    public int CompareMargin { get; private set; }
} 
Mając margines możemy określić jaka może być różnica między aktualną, a oczekiwaną wartością. Aby obliczyć ten margines będziemy musieli znać datę pisania testu (dateTimeCodeWritten) i na podstawie tej daty obliczamy ilość dni na różnicę. Gdybyśmy tą różnicę nie obliczyli, a wpisalibyśmy stałą wartość to mogłoby się okazać, że któregoś dnia wszystkie nasze testy przestałyby przechodzić.
[Test]
public void Check_if_GetDaysFromNow_Work_With_Comparer()
{
var dateTimeCodeWritten = new DateTime(2012, 06, 25);
DateTime date_20_06_2012 = new DateTime(2012, 06, 20);

var calc = new NowDateTimeCalculator(date_20_06_2012);

var dateTimeCodeWrittenDiffTimeSpan =
 DateTime.Now - dateTimeCodeWritten;

int dateTimeDiffDays = 
(int)dateTimeCodeWrittenDiffTimeSpan.TotalDays;



var dateTimeComparer = 
new DateTimeComparer(new TimeSpan(dateTimeDiffDays, 0, 0,0));

Assert.IsTrue(dateTimeComparer
.Compare(calc.dateTime.AddDays(5), DateTime.Now) == 0);


var intComp = new IntComparer(dateTimeDiffDays);

Assert.IsTrue(intComp
.Compare(calc.GetDaysFromNow(), 5) == 0);  
   
}
Nie jest to najlepiej napisany test, ale można przetestować DateTime.Now. Zaletą tej techniki jest to, że nie musimy znać (zmieniać) implementacji kodu.

Te rodzaje rozwiązań można zastosować dla wielu innych statycznych metod w tym danych środowiskowych takich jak Environment.MachineName czy Thread.Sleep().

Brak komentarzy:

Prześlij komentarz