我正在测试一个我需要一定时间才能通过的课程才能检查结果。具体来说,我需要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对象存储与数据库的连接以及一些相关数据。
答案 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));
}
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));
}
感谢大家对此的帮助。如果您发现更改有任何问题,请与我们联系。