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

(setAccessTime セッターまたはリフレクションを作成して) accessTime プライベート変数を変更するか、単体テストにスリープを挿入する以外に、これをテストすることは可能ですか?

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を渡すことができます。この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

テストされている機能は、期限切れのすべてのセッションを削除する SessionManager のようです。

DBSession を拡張するテスト クラスを作成することを検討します。

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

}
于 2012-04-05T06:54:13.730 に答える
1

編集:ギシュの答えが好きです。彼はまた、時間を嘲笑することを奨励していますが、彼はそれを一流のオブジェクトとして扱います.

テストしようとしているルールは正確には何ですか?私があなたのコードを正しく読んでいれば、ID「test」に関連付けられたセッションが特定のタイムアウト後に期限切れになることを確認したいというあなたの願いのようですね。

時間は本質的にグローバルな状態であるため、単体テストでは注意が必要です。したがって、これは受け入れテストのより良い候補です (zerkms が提案したように)。

それでも単体テストが必要な場合は、通常、時間への参照を抽象化および/または分離して、テストでそれらをモックできるようにします。これを行う 1 つの方法は、テスト対象のクラスをサブクラス化することです。これはカプセル化のわずかな中断ですが、保護されたセッター メソッドを提供するよりもクリーンに動作し、リフレクションよりもはるかに優れています。

例:

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() メソッドは決してテストされず、テストが実装について何かを知る必要があるからです。そのため、そのようなメソッドを小さくして焦点を絞ったままにして、副作用をなくすのが良い理由です。この例では、テスト対象のクラスが final ではないことも前提としています。

欠点はありますが、プログラマーが実際に害を及ぼす可能性があるプライベート変数にスコープ セッターを提供するよりもクリーンです (私の意見では)。私の例では、不正なプロセスが getNow() メソッドを呼び出しても、実際に害はありません。

于 2012-04-05T05:43:32.920 に答える
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) {

その後、ギシュが提案したものと同様のコードを使用できました(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() で問題が発生しました。ここでは、1 つのセッションを期限切れにしたいが、もう 1 つのセッションは期限切れにしないようにしました。このリンク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 に答える