如何使用Mockito在Java中模拟new Date()?

57
我有一个函数,它使用当前时间进行一些计算。我想使用Mockito来模拟它。
我想测试的类的示例:
public class ClassToTest {
    public long getDoubleTime(){
        return new Date().getTime()*2;
    }
}

我想要像这样的东西:

@Test
public void testDoubleTime(){
   mockDateSomeHow(Date.class).when(getTime()).return(30);
   assertEquals(60,new ClassToTest().getDoubleTime());
}

能够模拟那个吗?我不想改变“被测试”的代码以便进行测试。


3
为什么你不改变已经测试过的代码呢?更易于进行测试的代码通常耦合度更低……你为什么不想要这样的代码呢? - blank
可能是覆盖Java System.currentTimeMillis的重复问题。 - Jon Skeet
还有一件事——更改“已测试”的代码很容易——你有测试可以告诉你犯了什么错误——但是更改未经测试的代码另当别论……这时你需要用到Michael Feathers的功夫 ;) - blank
11
如果你想再约会的话,嘲笑一个新的约会不是一个好策略 :-) - Stephen C
1
请阅读我在https://dev59.com/EGTWa4cB1Zd3GeqPIv90 上的回答,这是一个类似(但不完全相同)的问题。 - Dawood ibn Kareem
9
@StephenC - 哈哈!我不得不读你的评论三遍才意识到这是个笑话。 - Dawood ibn Kareem
7个回答

71

正确的做法是重新组织你的代码,使其更易测试,如下所示。重构你的代码以消除对日期的直接依赖将允许你注入不同的实现来适应正常运行时和测试运行时:

interface DateTime {
    Date getDate();
}

class DateTimeImpl implements DateTime {
    @Override
    public Date getDate() {
       return new Date();
    }
}

class MyClass {

    private final DateTime dateTime;
    // inject your Mock DateTime when testing other wise inject DateTimeImpl

    public MyClass(final DateTime dateTime) {
        this.dateTime = dateTime;
    }

    public long getDoubleTime(){
        return dateTime.getDate().getTime()*2;
    }
}

public class MyClassTest {
    private MyClass myClassTest;

    @Before
    public void setUp() {
        final Date date = Mockito.mock(Date.class);
        Mockito.when(date.getTime()).thenReturn(30L);

        final DateTime dt = Mockito.mock(DateTime.class);
        Mockito.when(dt.getDate()).thenReturn(date);

        myClassTest = new MyClass(dt);
    }

    @Test
    public void someTest() {
        final long doubleTime = myClassTest.getDoubleTime();
        assertEquals(60, doubleTime);
    }
}

1
我同意。我一直都是这样做的。效果很棒,对原始代码的更改很小,测试也很容易。 - stmax
29
这是解决这个问题的经典方法(你所称的“DateTime”更能描述为“Clock”或类似的名称)。然而,这确实意味着需要重构你的代码并增加一些复杂性,纯粹是为了进行测试,这有点像代码异味。 - Tom Anderson
4
所以我这样做了,我认为这是一个好的方法,但问题是如何使用Mockito来实现它 :D - Jordi P.S.
1
甚至更好的做法是:使用JodaTime。 - bric3
3
很遗憾,这个回答并没有解答问题。如果我的代码使用了一个库,这个库又使用了另一个库来调用 new Date(),而我的测试因为测试向量包含了一个有效期直到昨天的证书而停止工作,那么我不知道该如何从库中移除 new Date() 来快速修复我的测试。 - Giszmo
显示剩余6条评论

29

如果您有无法重构且不想影响 System.currentTimeMillis() 的旧代码,请尝试使用 PowermockPowerMockito

//note the static import
import static org.powermock.api.mockito.PowerMockito.whenNew;

@PrepareForTest({ LegacyClassA.class, LegacyClassB.class })

@Before
public void setUp() throws Exception {

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    sdf.setTimeZone(TimeZone.getTimeZone("PST"));

    Date NOW = sdf.parse("2015-05-23 00:00:00");

    // everytime we call new Date() inside a method of any class
    // declared in @PrepareForTest we will get the NOW instance 
    whenNew(Date.class).withNoArguments().thenReturn(NOW);

}

public class LegacyClassA {
  public Date getSomeDate() {
     return new Date(); //returns NOW
  }
}

或者更好的选择是使用 JMockit(可以把 Mockito 和 PowerMock 的功能合并在一起),这样您就不需要两种不同的测试框架了。 - Miyagi
有人能帮我解决这个问题吗?https://dev59.com/XM77oIgBc1ULPQZFKNPB - mattsmith5

6

1
我对如何做这样的事情很感兴趣,这就是问题的目的。你有例子吗? - Jordi P.S.
这来自于Powermock文档。在使用PowerMockito时应该是一样的。 - Brad
5
盲目地将每个“新”转换为包装对象,并通过注入的依赖项引入间接性,只会使代码变得冗长且难以理解。如果您正在类内创建ArrayList或HashMap,您现在会创建ArrayListFactory或HashMapFactory并将其注入到您的类中吗?盲目地在每次使用“new”时都使用PowerMock可能会创建一个非常紧密耦合的系统。能够判断像PowerMock这样的工具何时适用是成为熟练开发人员的一部分。 - Aneesh
2
显然,现在无法再对System类进行嘲讽了(我正在使用Mockito 3.12)。它说:“无法对java.lang.System的静态方法进行嘲讽,以避免干扰类加载,导致无限循环”。 - undefined
1
显然,你不能再嘲笑System类了(我正在使用Mockito 3.12)。它说:“无法模拟java.lang.System的静态方法,以避免干扰类加载,从而导致无限循环”。 - Daniel Beer

1
一种解决方案,虽然不能直接回答问题但可能解决潜在问题(测试可重复性),就是允许将 Date 作为测试参数,并向默认日期添加委托。

如下所示:

public class ClassToTest {

    public long getDoubleTime() {
      return getDoubleTime(new Date());
    }

    long getDoubleTime(Date date) {  // package visibility for tests
      return date.getTime() * 2;
    }
}

在生产代码中,您需要使用getDoubleTime()并测试getDoubleTime(Date date)

1

使用PowerMockito的new Date()System.currentTimeMillis()的工作示例。
这是一个关于实例的示例。

@RunWith(PowerMockRunner.class)
@PrepareForTest(LegacyClass.class) // prepares byte-code of the LegacyClass
public class SystemTimeTest {
    
    private final Date fakeNow = Date.from(Instant.parse("2010-12-03T10:15:30.00Z"));

    @Before
    public void init() throws Exception {
        // mock new Date()
        PowerMockito.whenNew(Date.class).withNoArguments().thenReturn(fakeNow);
        System.out.println("Fake now: " + fakeNow);

        // mock System.currentTimeMillis()
        PowerMockito.mockStatic(System.class);
        PowerMockito.when(System.currentTimeMillis()).thenReturn(fakeNow.getTime());
        System.out.println("Fake currentTimeMillis: " + System.currentTimeMillis());
    }

    @Test
    public void legacyClass() {
        LegacyClass legacyClass = new LegacyClass();
        legacyClass.methodWithNewDate();
        legacyClass.methodWithCurrentTimeMillis();
    }

}

class LegacyClass {

    public void methodWithNewDate() {
        Date now = new Date();
        System.out.println("LegacyClass new Date() is " + now);
    }

    public void methodWithCurrentTimeMillis() {
        long now = System.currentTimeMillis();
        System.out.println("LegacyClass System.currentTimeMillis() is " + now);
    }

}

控制台输出

Fake now: Fri Dec 03 16:15:30 NOVT 2010
Fake currentTimeMillis: 1291371330000
LegacyClass new Date() is Fri Dec 03 16:15:30 NOVT 2010
LegacyClass System.currentTimeMillis() is 1291371330000

0

你也可以使用jmockit来模拟new Date():

    @Test
    public void mockTime() {
        new MockUp<System>() {
            @Mock
            public long currentTimeMillis() {
                // Now is always 11/11/2021
                Date fake = new Date(121, Calendar.DECEMBER, 11);
                return fake.getTime();
            }
        };
        Assert.assertEquals("mock time failed", new Date(121, Calendar.DECEMBER, 11), new Date());
    }

-1
Date now = new Date();    
now.set(2018, Calendar.FEBRUARY, 15, 1, 0); // set date to 2018-02-15
//set current time to 2018-02-15
mockCurrentTime(now.getTimeInMillis());

private void mockCurrentTime(long currTimeUTC) throws Exception {
    // mock new dates with current time
    PowerMockito.mockStatic(Date.class);
    PowerMockito.whenNew(Date.class).withNoArguments().thenAnswer(new Answer<Date>() {

        @Override
        public Date answer(InvocationOnMock invocation) throws Throwable {
            return new Date(currTimeUTC);
        }
    });

    //do not mock creation of specific dates
    PowerMockito.whenNew(Date.class).withArguments(anyLong()).thenAnswer(new Answer<Date>() {

        @Override
        public Date answer(InvocationOnMock invocation) throws Throwable {
            return new Date((long) invocation.getArguments()[0]);
        }
    });

    // mock new calendars created with time zone
    PowerMockito.mockStatic(Calendar.class);
    Mockito.when(Calendar.getInstance(any(TimeZone.class))).thenAnswer(new Answer<Calendar>() {
        @Override
        public Calendar answer(InvocationOnMock invocation) throws Throwable {
            TimeZone tz = invocation.getArgumentAt(0, TimeZone.class);
            Calendar cal = Calendar.getInstance(tz);
            cal.setTimeInMillis(currTimeUTC);
            return cal;
        }
    });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接