如何在不访问私有变量的情况下测试jUnit测试中的时间传递?

时间:2012-04-05 03:09:23

标签: java unit-testing junit tdd

我正在测试一个我需要一定时间才能通过的课程才能检查结果。具体来说,我需要x分钟才能通过才能判断测试是否有效。我已经读过在单元测试中我们应该测试接口而不是实现,所以我们不应该访问私有变量,但除了在我的单元测试中进行休眠之外,我不知道如何在不修改私有变量的情况下进行测试。

我的测试设置如下:

@Test
public void testClearSession() {
    final int timeout = 1;
    final String sessionId = "test";
    sessionMgr.setTimeout(timeout);
    try {
        sessionMgr.createSession(sessionId);
    } catch (Exception e) {
        e.printStackTrace();
    }
    DBSession session = sessionMgr.getSession(sessionId);
    sessionMgr.clearSessions();
    assertNotNull(sessionMgr.getSession(sessionId));
    Calendar accessTime = Calendar.getInstance();
    accessTime.add(Calendar.MINUTE, - timeout - 1);
    session.setAccessTime(accessTime.getTime()); // MODIFY PRIVATE VARIABLE VIA PROTECTED SETTER
    sessionMgr.clearSessions();
    assertNull(sessionMgr.getSession(sessionId));
}

除了修改accessTime私有变量(通过创建setAccessTime setter或反射)或在单元测试中插入sleep之外,是否可以测试这个?

2012年4月11日编辑

我特意尝试测试我的SessionManager对象在经过一段特定时间后清除会话。我连接的数据库将在一段固定的时间后丢弃连接。当我接近该超时时,SessionManager对象将通过在数据库上调用“finalize session”过程并从其内部列表中删除会话来清除会话。

SessionManager对象旨在在单独的线程中运行。我正在测试的代码如下所示:

public synchronized void clearSessions() {
    log.debug("clearSessions()");
    Calendar cal = Calendar.getInstance();
    cal.add(Calendar.MINUTE, - timeout);
    Iterator<Entry<String, DBSession>> entries = sessionList.entrySet().iterator();
    while (entries.hasNext()) {
        Entry<String, DBSession> entry = entries.next();
        DBSession session = entry.getValue();
        if (session.getAccessTime().before(cal.getTime())) {
            // close connection
            try {
                connMgr.closeconn(session.getConnection(), entry.getKey());
            } catch (Exception e) {
                e.printStackTrace();
            }
            entries.remove();
        }
    }
}

对connMgr(ConnectionManager对象)的调用有点复杂,但我正在重构遗留代码的过程,而这正是它目前的情况。 Session对象存储与数据库的连接以及一些相关数据。

4 个答案:

答案 0 :(得分:4)

  • 测试可以通过一些重构来使意图更清晰。如果我理解的是正确的......

public void TestClearSessionsMaintainsSessionsUnlessLastAccessTimeIsOverThreshold() {

    final int timeout = 1;
    final String sessionId = "test";
    sessionMgr = GetSessionManagerWithTimeout(timeout);
    DBSession session = CreateSession(sessionMgr, sessionId);

    sessionMgr.clearSessions();
    assertNotNull(sessionMgr.getSession(sessionId));

    session.setAccessTime(PastInstantThatIsOverThreshold()); // MODIFY PRIVATE VARIABLE VIA PROTECTED SETTER
    sessionMgr.clearSessions();
    assertNull(sessionMgr.getSession(sessionId));
}
  • 现在进行测试而不必暴露私有状态
    • 如何在现实生活中修改私有变量?是否有其他一些可以调用的公共方法可以更新访问时间?
    • 由于时钟/时间是一个重要的概念,为什么不把它作为一个角色。因此,您可以将Clock对象传递给Session,它用于更新其内部访问时间。在测试中,您可以传入一个MockClock,其getCurrentTime()方法将返回您希望的任何值。我正在编写模拟语法..所以用你正在使用的任何内容进行更新。

public void TestClearSessionsMaintainsSessionsUnlessLastAccessTimeIsOverThreshold() {

      final int timeout = 1;
      final String sessionId = "test";
      expect(mockClock).GetCurrentTime(); willReturn(CurrentTime());
      sessionMgr = GetSessionManagerWithTimeout(timeout, mockClock);
      DBSession session = CreateSession(sessionMgr, sessionId);

      sessionMgr.clearSessions();
      assertNotNull(sessionMgr.getSession(sessionId));

      expect(mockClock).GetCurrentTime(); willReturn(PastInstantThatIsOverThreshold());
      session.DoSomethingThatUpdatesAccessTime();
      sessionMgr.clearSessions();
      assertNull(sessionMgr.getSession(sessionId));
}

答案 1 :(得分:1)

编辑:我更喜欢Gishu的回答。他还鼓励你嘲笑时间,但他把它视为一流的对象。

您要测试的规则究竟是什么?如果我正确地阅读您的代码,看起来您希望验证与ID“test”相关联的会话在给定的超时后到期,是否正确?

时间在单元测试中是一件棘手的事情,因为它基本上是全局状态,所以这是一个更好的候选测试(如zerkms建议)。

如果您仍然希望对其进行单元测试,通常我会尝试抽象和/或隔离对时间的引用,因此我可以在测试中模拟它们。一种方法是通过子类化测试中的类。这是封装中的一个小小的突破,但它比提供受保护的setter方法更干净,并且比反射更好。

一个例子:

class MyClass {
  public void doSomethingThatNeedsTime(int timeout) {
    Date now = getNow();
    if (new Date().getTime() > now.getTime() + timeout) {
      // timed out!
    }
  }

  Date getNow() {
    return new Date();
  }
}

class TestMyClass {
  @Test
  public void testDoSomethingThatNeedsTime() {
    MyClass mc = new MyClass() {
      Date getNow() {
        // return a time appropriate for my test
      }    
    };

    mc.doSomethingThatNeedsTime(1);

    // assert
  }
}

这是一个人为的例子,但希望你明白这一点。通过继承getNow()方法,我的测试不再受全局时间的影响。我可以随时替换。

就像我说的那样,这会破坏封装,因为REAL getNow()方法永远不会被测试,并且需要测试才能知道有关实现的内容。这就是为什么保持这种方法小而集中,没有副作用的好处。此示例还假设被测试的类不是最终的。

尽管存在缺点,但它比为私有变量提供范围的setter更清晰(在我看来),这实际上可以让程序员做坏事。在我的例子中,如果某个流氓进程调用getNow()方法,那么就没有真正的伤害。

答案 2 :(得分:1)

看起来正在测试的功能是SessionManager驱逐所有过期的会话。

我会考虑创建扩展DBSession的测试类。

AlwaysExpiredDBSession extends DBSession  {
....
// access time to be somewhere older 'NOW'

}

答案 3 :(得分:0)

我基本上遵循了Gishu的建议https://stackoverflow.com/a/10023832/1258214,但我认为我会记录这些变化只是为了其他人阅读本文的利益(所以任何人都可以评论实施的问题)。感谢你的评论指向JodaTime和Mockito。

相关的想法是识别代码对时间的依赖性并将其提取出来(参见:https://stackoverflow.com/a/5622222/1258214)。这是通过创建一个接口来完成的:

import org.joda.time.DateTime;

public interface Clock {
    public DateTime getCurrentDateTime() ;
}

然后创建一个实现:

import org.joda.time.DateTime;

public class JodaClock implements Clock {

    @Override
    public DateTime getCurrentDateTime() {
        return new DateTime();
    }

}

然后将其传递给SessionManager的构造函数:

SessionManager(ConnectionManager connMgr, SessionGenerator sessionGen,
        ObjectFactory factory, Clock clock) {

然后我能够使用类似于Gishu建议的代码(注意testClear开头的小写't'...我的单元测试非常成功,大写'T'直到我意识到测试没跑......):

@Test
public void testClearSessionsMaintainsSessionsUnlessLastAccessTimeIsOverThreshold() {
    final String sessionId = "test";
    final Clock mockClock = mock(Clock.class);

    when(mockClock.getCurrentDateTime()).thenReturn(getNow());
    SessionManager sessionMgr = getSessionManager(connMgr,
            sessionGen, factory, mockClock);
    createSession(sessionMgr, sessionId);

    sessionMgr.clearSessions(defaultTimeout);
    assertNotNull(sessionMgr.getSession(sessionId));

    when(mockClock.getCurrentDateTime()).thenReturn(getExpired());

    sessionMgr.clearSessions(defaultTimeout);
    assertNull(sessionMgr.getSession(sessionId));
}

这很好,但是我删除了Session.setAccessTime(),在另一个测试testOnlyExpiredSessionsCleared()中创建了一个问题,我希望一个会话到期而不是另一个会话。这个链接https://stackoverflow.com/a/6060814/1258214让我想到了SessionManager.clearSessions()方法的设计,我重构了会话从SessionManager到达DBSession对象本身的过程。

自:

if (session.getAccessTime().before(cal.getTime())) {

要:

if (session.isExpired(expireTime)) {

然后我插入了一个mockSession对象(类似于Jayan的建议https://stackoverflow.com/a/10023916/1258214

@Test
public void testOnlyOldSessionsCleared() {
    final String sessionId = "test";
    final String sessionId2 = "test2";

    ObjectFactory mockFactory = spy(factory);
    SessionManager sm = factory.createSessionManager(connMgr, sessionGen,
        mockFactory, clock);

    // create expired session
    NPIISession session = factory.createNPIISession(null, clock);
    NPIISession mockSession = spy(session);
    // return session expired
    doReturn(true).when(mockSession).isExpired((DateTime) anyObject());

    // get factory to return mockSession to sessionManager
    doReturn(mockSession).when(mockFactory).createDBSession(
        (Connection) anyObject(), eq(clock));
    createSession(sm, sessionId);

    // reset factory so return normal session
    reset(mockFactory);
    createSession(sm, sessionId2);

    assertNotNull(sm.getSession(sessionId));
    assertNotNull(sm.getSession(sessionId2));

    sm.clearSessions(defaultTimeout);
    assertNull(sm.getSession(sessionId));
    assertNotNull(sm.getSession(sessionId2));
}

感谢大家对此的帮助。如果您发现更改有任何问题,请与我们联系。