【问题标题】:How to test passing of time in jUnit test without accessing private variables?如何在不访问私有变量的情况下测试 jUnit 测试中的时间传递?
【发布时间】:2012-04-05 03:09:23
【问题描述】:

我正在对一门课程进行单元测试,我需要一定的时间才能通过检查结果。具体来说,我需要通过 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 设置器或反射)或在单元测试中插入睡眠之外,是否可以对此进行测试?

2012 年 4 月 11 日编辑

我特别想测试我的 SessionManager 对象是否在特定时间段过后清除会话。我正在连接的数据库将在一段固定时间后断开连接。当我接近该超时时,SessionManager 对象将通过在数据库上调用“最终会话”过程来清除会话,并从其内部列表中删除会话。

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 对象存储与数据库的连接以及一些相关数据。

【问题讨论】:

  • 附注:单元测试通常不应该测试任何在后台工作的东西(在单独的线程中)。因此,您可以拆分逻辑并在另一个线程中运行它。然后第一个可以通过单元测试轻松测试,第二个 - 通过功能/验收测试
  • 您正在测试的代码是否调用System.getCurrentTimeMillis()(或类似的名称,例如new Date())?如果是这样,这就是一种可测试性反模式;您想将此委托给可以注入的“时间源”类。然后,您可以使用模拟时间源进行测试。如果您可以发布您尝试测试的源代码,我可以帮助您了解如何执行我的建议。
  • @DavidWallace - 我只是假设 GetInstance 就是那个电话。 JavaDocs 确认Calendar's getInstance method returns a Calendar object whose time fields have been initialized with the current date and time:
  • 对,但我认为你不能注入它; getInstance 是静态的。 OP 需要的是一个可以为您提供当前时间的类(可选为 Calendar),但也可以模拟并注入到 SUT 中。
  • 我建议在这里使用 Jodatime:它的 DateTimeUtils 类允许你模拟时间的来源。

标签: java unit-testing junit tdd


【解决方案1】:
  • 测试可以进行一些重构以使意图更清晰。如果我的理解是对的……

.

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

【讨论】:

  • 你应该提到你使用mockito 来获得漂亮的模拟语法。
  • @BjörnPollex 你确定吗?在我看来,它不像 mockito 语法。
  • +1 还用于将时钟公开为可以在测试中模拟并在生产中更改的依赖项。
  • @DavidWallace:我认为你是对的——我很困惑。应该是模仿虽然:)
  • @BjörnPollex - 我认为这是 JMock 的自定义混搭。自从我用 Java 模拟以来已经有一段时间了,但这并没有阻止我回答 SO :)
【解决方案2】:

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

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

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

}

【讨论】:

  • 这是一个有趣的可能性,尽管我的 DBSession 对象是由我正在测试的 SessionManager 对象创建的,所以我需要采取一些方法来注入该对象。我当前的界面允许创建和检索 DBSession,但不允许插入。我想我最终会遇到同样的问题,即必须修改 SessionManager 对象上的私有变量而不是 DBSession 对象的私有变量。
  • 我对此进行了思考,并意识到它会起作用。在一般情况下,这种方法肯定会起作用,但在我的情况下,我使用工厂创建 DBSession 对象,所以我只需将不同的工厂传递给 SessionManager,它应该可以工作。
【解决方案3】:

编辑:我更喜欢 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() 方法永远不会被测试,它需要测试来了解有关实现的一些信息。这就是为什么保持这种方法小而专注,没有副作用的原因。此示例还假设被测类不是最终类。

尽管有缺点,但(在我看来)它比为私有变量提供范围设置器更干净,这实际上可以让程序员造成伤害。在我的示例中,如果某个流氓进程调用 getNow() 方法,则不会造成真正的危害。

【讨论】:

  • 是的,我试图在关联的数据库连接超时之前捕获会话,所以我可以干净地关闭连接,而不是超时。感谢 cmets,不同的观点很有帮助。
【解决方案4】:

我基本上遵循 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));
}

感谢大家为此提供的帮助。如果您发现更改有任何问题,请告诉我。

【讨论】:

    猜你喜欢
    • 2015-03-07
    • 2011-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多