使用没有 DependencyInjection 的 Calendar.getInstance() 测试自定义 DateUtilClass

Test custom DateUtilClass with Calendar.getInstance() without DependencyInjection

考虑以下 class:

public class DateUtils {

    public static Date get31OfDecember2YearsAgo() {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int year2YearsAgo = calendar.get(Calendar.YEAR) - 2;
        calendar.set(year2YearsAgo, Calendar.DECEMBER, 31);
        return calendar.getTime();
    }
}

你知道我如何测试这段代码吗?我在考虑一个 DateProvider,我可以在测试中注入和模拟它,但是因为我没有可用的依赖注入...

此代码被硬编码为使用 CalendarDate,因此您无法按原样对其进行真正的测试。话又说回来,public static 方法通常不打算进行测试;相反,它们应该非常简单,您只需查看它们并确定它们是否正确。

我强烈建议您放弃此 get31ofDecember2YearsAgo() 方法以及整个 DateUtils class,并重构您的整个系统以开始使用正确设计的可实例化 class 用于日期和时间实用程序。这样做时,请使用最新的 java 8 时间 classes,或者,如果您不幸不得不使用 8 之前的某些版本的 java,那么至少使用 joda 时间库。

切勿在任何地方使用构思不当的Instant.now()方法;相反,创建一个提供当前时间的界面,以便您可以正确模拟它以进行测试。

编辑

这里,来自我正在从事的一个项目:

import java.time.*;

/**
 * Provides the notion of current time. Necessary, because without it, 
 * classes that deal with the current time cannot be tested.
 *
 * @author Michael Belivanakis (michael.gr)
 */
public interface TimekeepingDomain
{
    /**
     * Gets the current {@link Instant}.  (UTC, of course.)
     *
     * @return the current {@link Instant}.
     */
    Instant getCurrentInstant();

    /**
     * Gets the system {@link ZoneId}.
     *
     * @return the system {@link ZoneId}.
     */
    ZoneId getSystemZoneId();

    /**
     * Sleeps until the given {@link Instant}.
     *
     * Returns immediately if the given {@link Instant} is in the past.
     *
     * @param instant the {@link Instant} to sleep until.
     */
    void sleepUntil( Instant instant ) throws InterruptedException;

    /**
     * Sleeps for the given {@link Duration}.
     *
     * Returns immediately if the given {@link Duration} is negative.
     *
     * @param duration the {@link Duration} to sleep for.
     */
    void sleepFor( Duration duration ) throws InterruptedException;
}

一个"real"计时域实现:

final TimekeepingDomain timekeepingDomain = new TimekeepingDomain()
{
    @Override
    public Instant getCurrentInstant()
    {
        return Instant.now();
    }

    @Override
    public ZoneId getSystemZoneId()
    {
        return ZoneId.systemDefault();
    }

    @Override
    public void sleepUntil( Instant instant ) throws InterruptedException
    {
        sleepFor( Duration.between( Instant.now(), instant ) );
    }

    @Override
    public void sleepFor( Duration duration ) throws InterruptedException
    {
        if( duration.isNegative() )
            return;
        long milliseconds = duration.toMillis();
        Thread.sleep( milliseconds );
    }
};

一个"fake"计时域实现:

import java.time.*;

/**
 * Fake {@link TimekeepingDomain} for testing.
 *
 * @author Michael Belivanakis (michael.gr)
 */
class FakeTimekeepingDomain implements TimekeepingDomain
{
    private Instant currentInstant;

    FakeTimekeepingDomain( Instant startTime )
    {
        currentInstant = startTime;
    }

    @Override
    public Instant getCurrentInstant()
    {
        currentInstant = currentInstant.plusMillis( 1L );
        return currentInstant;
    }

    @Override
    public ZoneId getSystemZoneId()
    {
        return ZoneOffset.UTC;
    }

    @Override
    public void sleepUntil( Instant instant )
    {
        if( instant.isAfter( currentInstant ) )
            currentInstant = instant;
    }

    @Override
    public void sleepFor( Duration duration )
    {
        if( !duration.isNegative() )
            currentInstant = currentInstant.plus( duration );
    }
}

实例化如下:

TimekeepingDomain timekeepingDomain = 
        new FakeTimekeepingDomain( Instant.parse( "2014-08-11T10:15:30.00Z" ) );

那么接下来会发生什么,你的 calculateSomethingRelativeToNow() 方法变成非静态的,包含它的 class 被传递一个 TimekeepingDomain 作为构造函数参数,这样你的方法就可以调用该接口以获取(您希望它认为的)当前时间。

在您的生产设备中,您将 class 传递给 TimekeepingDomain 的真实实现的引用,一切都像以前一样工作。

在你的测试装置中,你可以传递你的 class 一个 FakeTimekeepingDomain 的实例,它已经被实例化以假装 'current' 时间是某个固定时间,(在过去, 或未来,都没有关系,) 这样您就可以检查您的方法的结果,看看它实际上是预期的结果。您可以尝试各种固定时间,以检查各种结果。

本质上,这是手动完成的依赖注入,没有使用像 Spring 这样的重型框架来实现它。

您可以重构 class 如下:

public class DateUtils {

  public static Date get31OfDecember2YearsAgo() {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(new Date()); // This line is surely redundant?
    return get31OfDecember2YearsAgo(calendar);
  }

  static Date get31OfDecember2YearsAgo(Calendar calendar) {        
    int year2YearsAgo = calendar.get(Calendar.YEAR) - 2;
    calendar.set(year2YearsAgo, Calendar.DECEMBER, 31);
    return calendar.getTime();
  }
}

这将允许您测试 package-private 变体并使用一些已知值来检查正确的结果。