2012-04-05 69 views
5

我正在測試一個班級,我需要一段時間才能檢查結果。具體而言,我需要x分鐘才能確定測試是否奏效。我已經讀過,在單元測試中,我們應該測試接口而不是實現,所以我們不應該訪問私有變量,但除了在我的單元測試中進行睡眠之外,我不知道如何在不修改私有變量的情況下進行測試。如何在不訪問私有變量的情況下測試jUnit測試中的時間傳遞?

我的測試設置是這樣的:

@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設定器或反射)在單元測試,或插入sleep ?

EDIT 11月 - 2012

我特別想測試的特定時間段過去之後我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對象)的調用是有點令人費解,但我在重構遺留代碼的過程,這是它當時是什麼。會話對象存儲到數據庫的連接以及一些關聯的數據。

+0

附註:單元測試一般不應該測試任何在後臺工作的東西(在單獨的線程中)。所以你可以分解邏輯並在另一個線程中運行它。第一個可以通過單元測試輕鬆測試,第二個 - 通過功能/驗收測試 – zerkms 2012-04-05 03:21:23

+0

您正在測試的代碼是否調用'System.getCurrentTimeMillis()'(或類似的東西'new Date()') )?如果是這樣,這是一種可測試性反模式;你想把它委派給一個「時間源」類,你可以注入。然後,您可以使用模擬時間源進行測試。如果您可以發佈您想要測試的源代碼,我可以幫助您瞭解如何執行我的建議。 – 2012-04-05 08:40:22

+0

@DavidWallace - 我只是假設GetInstance就是這個調用。 JavaDocs確認'Calendar的getInstance方法返回一個Calendar對象,它的時間字段已經用當前的日期和時間初始化了:' – Gishu 2012-04-05 11:20:31

回答

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對象到會話中,它用來更新它的內部訪問時間。在你的測試中,你可以傳入一個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)); 
} 
+0

對於測試方法名稱+1。 – Jayan 2012-04-05 06:55:19

+0

您應該提及您使用[mockito](http://code.google.com/p/mockito/)來獲得這個很好的嘲諷語法。 – 2012-04-05 07:08:15

+0

@BjörnPollex你確定嗎?它看起來不像我的mockito語法。 – 2012-04-05 08:27:48

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()方法,那麼沒有真正的傷害。

+0

是的,我想在關聯的數據庫連接超時之前趕上會話,所以我可以乾淨地關閉連接,而不是超時。感謝評論,不同的觀點是有幫助的。 – Allan5 2012-04-11 08:01:40

1

它看起來像被測試的功能是SessionManager evitcs所有過期的會話。

我會考慮創建擴展DBSession的測試類。

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

} 
+0

雖然我的DBSession對象是由我正在測試的SessionManager對象創建的,但這是一個有趣的可能性,所以我需要設法注入對象。我目前的界面允許創建和檢索DBSessions,但不允許插入。我想我最終會遇到同樣的問題:必須修改SessionManager對象上的私有變量而不是DBSession對象的私有變量。 – Allan5 2012-04-11 08:08:05

+0

我曾想過這件事,並意識到它會起作用。在一般情況下,這種方法肯定會起作用,但在我的情況下,我正在使用工廠創建DBSession對象,所以我只需要將不同的工廠傳遞給SessionManager,它應該可以工作。 – Allan5 2012-04-12 07:03:38

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對象(類似於傑恩的建議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)); 
} 

感謝大家對他們這方面的幫助。如果您看到有關更改的任何問題,請讓我知道。

相關問題