5

我正在对一门课程进行单元测试,我需要一定的时间才能检查结果。具体来说,我需要通过 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 对象存储与数据库的连接以及一些相关数据。

4

4 回答 4

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));
}
于 2012-04-05T06:44:50.920 回答
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() 方法永远不会被测试,它需要测试知道一些关于实现的东西。这就是为什么保持这种方法小而专注,没有副作用的原因。这个例子还假设被测试的类不是最终的。

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

于 2012-04-05T05:43:32.920 回答
1

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

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

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

}
于 2012-04-05T06:54:13.730 回答
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));
}

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

于 2012-05-01T07:07:35.767 回答