web-dev-qa-db-ja.com

シングルトンおよびユニットテスト

Effective Javaには、シングルトンの単体テストに関する次の記述があります

クラスをシングルトンにすると、そのタイプとして機能するインターフェースを実装しない限り、シングルトンを模擬実装に置き換えることができないため、クライアントをテストすることが難しくなります。

誰がこれがそうなのか説明できますか?

35
user2434

リフレクションを使用してシングルトンオブジェクトをリセットし、テストが相互に影響するのを防ぐことができます。

@Before
public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
   Field instance = MySingleton.class.getDeclaredField("instance");
   instance.setAccessible(true);
   instance.set(null, null);
}

参照: nit-testing-singletons

37
TedEd

モックにはインターフェイスが必要です。なぜなら、あなたがしていることは、実際の基礎となる振る舞いを、テストに必要なものを模倣する詐欺師に置き換えるからです。クライアントはインターフェース参照タイプのみを扱うため、実装が何であるかを知る必要はありません。

インターフェースなしでは具象クラスをモックすることはできません。テストクライアントがそれを知らない限り、振る舞いを置き換えることはできないからです。その場合、まったく新しいクラスです。

これは、シングルトンかどうかに関係なく、すべてのクラスに当てはまります。

9
duffymo

実際にはシングルトンアクセスパターンの実装に依存すると思います。

例えば

MySingleton.getInstance()

テストするのは非常に難しいかもしれません

MySingletonFactory mySingletonFactory = ...
mySingletonFactory.getInstance() //this returns a MySingleton instance or even a subclass

シングルトンを使用しているという事実に関する情報を提供しません。したがって、工場を自由に交換できます。

[〜#〜] note [〜#〜]:シングルトンは、アプリケーション内のそのクラスの1つのインスタンスのみで定義されますが、取得または保存の方法は静的である必要はありません手段。

7
Liviu T.

問題は、シングルトン自体をテストすることではありません。この本は、クラスをテストしようとしている場合、依存するシングルトンだと、おそらく問題があると言っています。

つまり、(1)シングルトンにインターフェースを実装させ、(2)そのインターフェースを使用してシングルトンをクラスに注入しない限りです。

たとえば、シングルトンは通常、次のように直接インスタンス化されます。

public class MyClass
{
    private MySingleton __s = MySingleton.getInstance() ;

    ...
}

MyClassは、自動テストが非常に困難になる場合があります。たとえば、@ BorisPavlovićが答えで指摘しているように、シングルトンの動作がシステム時間に基づいている場合、テストはシステム時間にも依存するようになり、たとえば、曜日。

ただし、シングルトンが「そのタイプとして機能するインターフェイスを実装する」場合、それを渡す限り、そのインターフェイスのシングルトン実装を引き続き使用できます。

public class SomeSingleton
    implements SomeInterface
{
    ...
}

public class MyClass
{
    private SomeInterface __s ;

    public MyClass( SomeInterface s )
    {
        __s = s ;
    }

    ...
}

...

MyClass m = new MyClass( SomeSingleton.getInstance() ) ;

MyClassのテストの観点から、SomeSingletonがシングルトンであるかどうかは気にしません。シングルトンの実装を含む、必要な他の実装を渡すこともできますが、 llは、テストから制御するある種のモックを使用します。

ところで、これはそれを行う方法ではありません:

public class MyClass
{
    private SomeInterface __s = SomeSingleton.getInstance() ;

    public MyClass()
    {
    }

    ...
}

実行時にも同じように機能しますが、テストのためにSomeSingletonに再び依存するようになりました。

7
Rodney Gitzel

とても簡単です。

単体テストでは、isolate your SUT(テストするクラス)を使用します。 bunch of classesをテストしたくないのは、それがnit -testingの目的を無効にするためです。 。

しかし、すべてのクラスがeverythingを実行するわけではありませんか?ほとんどのクラスは他のクラスを使用して作業を行い、他のクラスを仲介し、独自のビットを追加して最終結果を取得します。

ポイントは、SUTがどのように作業に依存するかは気にしないということです。 SUT これらのクラスで動作するがどうなのか気になります。 スタブまたはモックがSUTに必要なクラスである理由です。また、SUTのコンストラクターパラメーターとして渡すことができるため、これらのモックを使用できます。

シングルトンの場合-悪いことは、getInstance()メソッドにグローバルにアクセスできることです。つまり、通常は後で使用できるインターフェイスのdependingの代わりにwithinからクラスを呼び出しますmock。それが、SUTをテストしたいときにreplaceすることが不可能な理由です。

解決策は、卑劣なpublic static MySingleton getInstance()メソッドを使用するのではなく、dependinterfaceで使用することで、クラスで作業する必要があります。それを行うと、必要に応じてtest doublesを渡すことができます。

7
Yam Marcovic

シングルトンオブジェクトは、外部からの制御なしで作成されます。同じ本の他の章の1つで、Blochはenumsをデフォルトのシングルトン実装として使用することを提案しています。例を見てみましょう

_public enum Day {
  MON(2), TUE(3), WED(4), THU(5), FRI(6), SAT(7), Sun(1);

  private final int index;

  private Day(int index) {

    this.index = index;
  }

  public boolean isToday() {

    return index == new GregorianCalendar().get(Calendar.DAY_OF_WEEK);
  }
}
_

週末にのみ実行されるべきコードがあるとしましょう:

_public void leisure() {

  if (Day.SAT.isToday() || Day.Sun.isToday()) {

    haveSomeFun();
    return;
  }

  doSomeWork();
}
_

レジャー方法のテストはかなり難しいでしょう。その実行は、実行される日に依存します。曜日に実行するとdoSomeWork()が呼び出され、週末にhaveSomeFun()が呼び出されます。

この場合、PowerMockなどの重いツールを使用してGregorianCalendarコンストラクターをインターセプトし、leisureメソッドの両方の実行パスをテストする2つのテストケースで平日または週末に対応するインデックスを返すモックを返す必要があります。

3
Boris Pavlović
it’s impossible to substitute a mock implementation for a singleton

本当じゃない。シングルトンをサブクラス化し、セッターがモックを挿入できます。あるいは、 PowerMock を使用して静的メソッドをモックできます。ただし、シングルトンをモックする必要性は、設計が不十分であることを示す場合があります。

実際の問題は、abuseddependency magnetsに変わるときのシングルトンです。それらはどこからでもアクセスできるため、適切なクラスに委任するよりも、特にOOPを初めて使用するプログラマーに必要な関数を配置する方がappearより便利です。

テスト容易性の問題は、テスト対象のオブジェクトからアクセスされるシングルトンの束を持っていることです。オブジェクトはおそらくシングルトンのメソッドのごく一部しか使用していませんが、各シングルトンをモックし、どのメソッドが依存しているかを把握する必要があります。静的状態(モノステートパターン)のシングルトンは、オブジェクト間のどの相互作用がシングルトンの状態の影響を受けるかを把握する必要があるため、さらに悪化します。

慎重に使用すると、シングルトンとテスト容易性が同時に発生する可能性があります。たとえば、DIフレームワークがない場合、ファクトリーおよびサービスロケーターとしてシングルトンを使用できます。これらを設定して、エンドツーエンドテスト用の偽のサービスレイヤーを作成できます。

3
Garrett Hall

可能です。 を参照してください

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import Java.lang.reflect.Field;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class DriverSnapshotHandlerTest {

private static final String MOCKED_URL = "MockedURL";
private FormatterService formatter;

@SuppressWarnings("javadoc")
@Before
public void setUp() {
    formatter = mock(FormatterService.class);
    setMock(formatter);
    when(formatter.formatTachoIcon()).thenReturn(MOCKED_URL);
}

/**
 * Remove the mocked instance from the class. It is important, because other tests will be confused with the mocked instance.
 * @throws Exception if the instance could not be accessible
 */
@After
public void resetSingleton() throws Exception {
   Field instance = FormatterService.class.getDeclaredField("instance");
   instance.setAccessible(true);
   instance.set(null, null);
}

/**
 * Set a mock to the {@link FormatterService} instance
 * Throws {@link RuntimeException} in case if reflection failed, see a {@link Field#set(Object, Object)} method description.
 * @param mock the mock to be inserted to a class
 */
private void setMock(FormatterService mock) {
    Field instance;
    try {
        instance = FormatterService.class.getDeclaredField("instance");
        instance.setAccessible(true);
        instance.set(instance, mock);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

/**
 * Test method for {@link com.example.DriverSnapshotHandler#getImageURL()}.
 */
@Test
public void testFormatterServiceIsCalled() {
    DriverSnapshotHandler handler = new DriverSnapshotHandler();
    String url = handler.getImageURL();

    verify(formatter, atLeastOnce()).formatTachoIcon();
    assertEquals(MOCKED_URL, url);
}

}
2
Kyrylo Semenko

私の知る限り、シングルトンを実装するクラスは拡張できません(スーパークラスコンストラクターは常に暗黙的に呼び出され、シングルトンのコンストラクターはプライベートです)。クラスをモックしたい場合は、クラスを拡張する必要があります。このケースでわかるように、それは不可能です。

0
DPM

シングルトン(および静的メソッド)の問題は、実際のコードをモック化された実装で置き換えることが困難になることです。

たとえば、次のコードを考えます

public class TestMe() {
  public String foo(String data) {
    boolean isFeatureFlag = MySingletonConfig.getInstance().getFeatureFlag();
    if (isFeatureFlag)
      // do somethine with data
    else
      // do something else with the data
     return result;
  }
}

Fooメソッドの単体テストを作成し、正しい動作が実行されていることを確認するのは簡単ではありません。これは、getFeatureFlagの戻り値を簡単に変更できないためです。

静的メソッドにも同じ問題が存在します-実際のターゲットクラスメソッドを模擬動作に置き換えるのは簡単ではありません。

確かに、 powermock などの回避策、メソッドへの依存性注入、またはテストでのリフレクションがあります。しかし、そもそもシングルトンを使用しない方がずっと良い

0
Tzafrir