静的メソッドには強力な機能が1つありますが、同時に非常に望ましくない機能があります。コードのどこからでも呼び出すことができ、実際には呼び出しの順序を調整する機能はありません。 多くの場合、このような制御は非常に重要ですが、順序があまり意味をなさない場合があります。 たとえば、ユニットテストで非常に厳密な順序でチェックを実行することはできません。 また、テストされたユニットですべてのチェックが完了することを保証するために、同じ静的メソッドverifyNoMoreInteractions(...)
がMockitoに存在します 。 場合によっては、最後のverify(...)
前であっても、このようなメソッドを誤って呼び出して、悔しさで「赤」テストを観察することができます。 しかし、チェックの実行に関する懸念をコンパイラ自体にシフトするとどうなりますか?
テスト対象の特定のモジュールに次のテストがあると仮定します。
public abstract class AbstractStructuredLoggingTest<T> { private final IStructuredLogger mockStructuredLogger = mock(IStructuredLogger.class); private T unit; @Nonnull protected abstract T createUnit(@Nonnull IStructuredLogger logger); protected final IStructuredLogger getMockStructuredLogger() { return mockStructuredLogger; } protected final T getUnit() { return unit; } @Before public void initializeMockStructuredLogger() { // . `selfAnswer()` -- , fluent- when(mockStructuredLogger.begin()).thenAnswer(selfAnswer()); when(mockStructuredLogger.put(any(LogEntryKey.class), any(Object.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.log(any(Scope.class), any(Severity.class), any(String.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.end()).thenAnswer(selfAnswer()); // ( , ). -- . unit = createUnit(mockStructuredLogger); } @After public void resetMockStructuredLogger() { try { // , verifyNoMoreInteractions(mockStructuredLogger); } finally { // , , ... reset(mockStructuredLogger); } } }
public final class AdministratorServiceStructuredLoggingTest extends AbstractStructuredLoggingTest<IAdministratorService> { private static final String USERNAME = "john.doe"; private static final String PASSWORD = "opZK2lkXa"; private static final String FIRST_NAME = "john"; private static final String LAST_NAME = "doe"; private static final String EMAIL = "john.doe@acme.com"; @Nonnull protected IAdministratorService createUnit(@Nonnull final IStructuredLogger logger) { return createAdministratorService(logger); } @Test public void testCreate() { final T unit = getUnit(); unit.create(USERNAME, PASSWORD, FIRST_NAME, LAST_NAME, EMAIL); final IStructuredLogger mockStructuredLogger = getMockStructuredLogger(); verify(mockStructuredLogger).put(eq(OPERATION_CALLER_CLASS), any(IAdministratorService.class)); verify(mockStructuredLogger).put(eq(OPERATION_CALLER_METHOD), any(Method.class)); verify(mockStructuredLogger).put(eq(OPERATION_TYPE), eq(CREATE)); verify(mockStructuredLogger).put(eq(OPERATION_OBJECT_TYPE), eq(ADMINISTRATOR)); verify(mockStructuredLogger).put(eq(VALUE_ADMINISTRATOR_NAME), eq(USERNAME)); verify(mockStructuredLogger).put(eq(VALUE_FIRST_NAME), eq(FIRST_NAME)); verify(mockStructuredLogger).put(eq(VALUE_LAST_NAME), eq(LAST_NAME)); verify(mockStructuredLogger).put(eq(VALUE_EMAIL), eq(EMAIL)); verify(mockStructuredLogger).log(eq(APP_DEV), eq(INFO), any(String.class)); verifyNoMoreInteractions(mockStructuredLogger); } }
このテストを正確にテストするものを推測することは難しくありません。テストされたユニットメソッドが構造化ロガーのすべての重要なメソッドを呼び出したかどうかをチェックします。 テストの最後にはverifyNoMockInteractions(...)
呼び出されるため、mokにはチェックが書き込まれていないメソッドが残っていないことが保証されます。 ところで、構造化されたロガーのインターフェースは非常にシンプルですが、コードは実際のプロジェクトから取得されているため、ここでは多少切り捨てた形で説明します。
public interface IStructuredLogger { // , , . @Nonnull IStructuredLogger begin() throws IllegalStateException; // , . // key -- (enum) (OPERATION_CALLER_CLASS, VALUE_FIRST_NAME ..) // value -- @Nonnull IStructuredLogger put(@Nonnull LogEntryKey key, @Nullable Object value) throws IllegalStateException; // . // scope -- (, APP_DEV -- ) // severity -- , , (ERROR, INFO ..) // message -- , @Nonnull IStructuredLogger log(@Nonnull Scope scope, @Nonnull Severity severity, @Nonnull String message) throws IllegalStateException; // begin() @Nonnull IStructuredLogger end() throws IllegalStateException; }
上記のように、テストにドットを付ける静的メソッドは、すべてのタイプのチェックが実行されることを保証しません。 そして、確かに、そのようなテストは失敗します。 このテストのチェックの種類とは、次のことを判断できることを意味します。
- ログ内のイベントがバインドされているクラス。
- コミットされたアクションとオブジェクトの種類。
- イベントログに記録されたアクションの引数。
- ジャーナリングプロセスに物理的に関与した雑誌。
- ユニット方式の構造化ロガーは、何にも使用されなくなりました。
実際、特定の順序で実行されるようにすることができる要件の有限セットがあります。
この問題を解決するには、戦略テンプレートを使用してオプションを検討します。各タイプのチェック用のメソッドを持つ特定のインターフェイスがあり、各メソッドがロギングの独自の側面を担当します。 または、たとえば、 テンプレートmethod 。 しかし、そのようなアプローチは非常に面倒であり、それらに対応する方法に従ってアスペクトの分離を保証するという点で信頼できないことは明らかです。 はい。読みやすさを犠牲にしなければなりません。
約5年前、私は、いくつかのそれほど明白ではない手法を使用して、複雑なオブジェクトの作成が正しい順序で実行されることを保証するビルダーテンプレートの実装を説明するインターネット上の記事に出会いました。 これは次のことを意味します。特定のオブジェクトビルダーでは、最初にsetFoo()
メソッドのみを呼び出し、次にsetBar()
てからbuild()
呼び出すことができます。 コンパイラーは順序に従うため、他の方法ではありません!
テンプレートメソッドを使用せずに、上記と同じ順序で正確に同じ順序でテストを書くことを簡素化するために、異なる実装での同様のアプローチを使用することもできます。 この場合のいくつかの形式化されたテスト機能を考えると、そのような遷移の結合に関与するそのようなインターフェースのセットを作成できます。 また、利便性のために、 流動的なインターフェイスを使用して、エレガントなチェックチェーンを構築できます。
// , , @FunctionalInterface public interface IOperationCallerVerificationStep { // unitMatcherSupplier -- // methodMatcherSupplier -- , // , -- @Nonnull IOperationTypeVerificationStep withOperationCaller( @Nonnull Supplier<?> unitMatcherSupplier, @Nonnull Supplier<Method> methodMatcherSupplier ); // , , . // , -. // (, , [APT](http://docs.oracle.com/javase/7/docs/technotes/guides/apt/)) // . , __ // Method, . @Nonnull default IOperationTypeVerificationStep withOperationCaller( @Nonnull final Supplier<?> unitMatcherSupplier ) { return withOperationCaller(unitMatcherSupplier, () -> any(Method.class)); } }
// , @FunctionalInterface public interface IOperationTypeVerificationStep { // operationTypeMatcherSupplier -- // objectTypeMatcherSupplier -- , @Nonnull IValueVerificationStep withOperationType( @Nonnull Supplier<OperationType> operationTypeMatcherSupplier, @Nonnull Supplier<ObjectType> objectTypeMatcherSupplier ); }
// , (.., , ) public interface IValueVerificationStep { // logEntryKeyMatcherSupplier -- // valueMatcherSupplier -- // , , // -- .., , // . @Nonnull IValueVerificationStep withValue( @Nonnull Supplier<LogEntryKey> logEntryKeyMatcherSupplier, @Nonnull Supplier<?> valueMatcherSupplier ); // . , . @Nonnull ILogVerificationStep then(); }
// , , @FunctionalInterface public interface ILogVerificationStep { // scopeMatcherSupplier -- // severityMatcherSupplier -- // messageMatcherSupplier -- // , void withLog( @Nonnull Supplier<Scope> scopeMatcherSupplier, @Nonnull Supplier<Severity> severityMatcherSupplier, @Nonnull Supplier<String> messageMatcherSupplier ); // default void withLog( @Nonnull final Supplier<Scope> scopeMatcherSupplier, @Nonnull final Supplier<Severity> severityMatcherSupplier ) { withLog(scopeMatcherSupplier, severityMatcherSupplier, () -> any(String.class)); } // , ( APP, DEV) default void withLog( @Nonnull final Supplier<Severity> severityMatcherSupplier ) { withLog(() -> eq(APP_DEV), severityMatcherSupplier, () -> any(String.class)); } }
ほとんどのインターフェイスは@FunctionalInterfaceとして注釈が付けられていますが、これは必須ではありません。 ただし、「variadic」インターフェイスには2つのメソッドがあります。操作引数のロギングのチェックの完了について何らかの方法で通知する必要があるためです。 したがって、元のテストコードは次の形式を取ることができます。
public abstract class AbstractStructuredLoggingTest<T> { private final IStructuredLogger mockStructuredLogger = mock(IStructuredLogger.class); private T unit; @Nonnull protected abstract T createUnit(@Nonnull IStructuredLogger logger); // -! , /*protected final IStructuredLogger getMockStructuredLogger() { return mockStructuredLogger; }*/ protected final T getUnit() { return unit; } @Before public void initializeMockStructuredLogger() { when(mockStructuredLogger.begin()).thenAnswer(selfAnswer()); when(mockStructuredLogger.put(any(LogEntryKey.class), any(Object.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.log(any(Scope.class), any(Severity.class), any(String.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.end()).thenAnswer(selfAnswer()); unit = createUnit(mockStructuredLogger); } @After public void resetMockStructuredLogger() { try { verifyNoMoreInteractions(mockStructuredLogger); } finally { reset(mockStructuredLogger); } } // , . . // , . // -. , verify(...), // . verifyNoMoreInteractions , // . protected final IOperationCallerVerificationStep verifyLog() { return (unitMatcherSupplier, methodMatcherSupplier) -> { verify(mockStructuredLogger).put(eq(OPERATION_CALLER_CLASS), unitMatcherSupplier.get()); verify(mockStructuredLogger).put(eq(OPERATION_CALLER_METHOD), methodMatcherSupplier.get()); return (IOperationTypeVerificationStep) (operationTypeMatcherSupplier, objectTypeMatcherSupplier) -> { verify(mockStructuredLogger).put(eq(OPERATION_TYPE), operationTypeMatcherSupplier.get()); verify(mockStructuredLogger).put(eq(OPERATION_OBJECT_TYPE), objectTypeMatcherSupplier.get()); return new IValueVerificationStep() { @Nonnull @Override public IValueVerificationStep withValue(@Nonnull final Supplier<LogEntryKey> logEntryKeyMatcherSupplier, @Nonnull final Supplier<?> valueMatcherSupplier) { verify(mockStructuredLogger).put(logEntryKeyMatcherSupplier.get(), valueMatcherSupplier.get()); return this; } @Nonnull @Override public ILogVerificationStep then() { return (scopeMatcherSupplier, severityMatcherSupplier, messageMatcherSupplier) -> verify(mockStructuredLogger).log(scopeMatcherSupplier.get(), severityMatcherSupplier.get(), messageMatcherSupplier.get()); } }; }; }; } }
さて、実際には、単純化そのものですが、そのためにテストの基本的な機能は複雑でした。
public final class AdministratorServiceStructuredLoggingTest extends AbstractStructuredLoggingTest { private static final String USERNAME = "usr"; private static final String PASSWORD = "qwerty"; private static final String FIRST_NAME = "john"; private static final String LAST_NAME = "doe"; private static final String EMAIL = "usr@mail.com"; @Nonnull protected IAdministratorService createUnit(@Nonnull final IStructuredLogger logger) { return createAdministratorService(logger); } @Test public void testCreate() { final T unit = getUnit(); unit.create(USERNAME, PASSWORD, FIRST_NAME, LAST_NAME, EMAIL); verifyLog() .withOperationCaller(() -> any(IAdministratorService.class)) .withOperationType(() -> eq(CREATE), () -> eq(ADMINISTRATOR)) .withValue(() -> eq(VALUE_ADMINISTRATOR_NAME), () -> eq(USERNAME)) .withValue(() -> eq(VALUE_FIRST_NAME), () -> eq(FIRST_NAME)) .withValue(() -> eq(VALUE_LAST_NAME), () -> eq(LAST_NAME)) .withValue(() -> eq(VALUE_EMAIL), () -> eq(EMAIL)) .then() .withLog(() -> eq(INFO)); } }
私の場合、コードはより信頼性が高く、非常に美しくなりました。 はい、より便利です。 そして最も重要なのは、ポイントをクリックすると、スマートIDEが次のステップがどうあるべきかを示します。 このようにして、コンパイラとIDEの両方が、テストがどれだけうまく書かれているかについてさらに自信を付けます。 ところで、なぜSupplier
とラムダ式が使用されるのですか? 実際、Mockitoはスタブがmokiに直接転送されるかどうかを確認し、そうでない場合は例外をスローします。 実際、ここでは、私の知る限り、ルールはもう少し複雑です。たとえば、Mockitoは匿名クラスを無視します。 この事実を考慮して、小さな抜け穴があります。Mockitoはreturn
を介したマッチャーの転送を追跡しないため、ラムダを使用する方法が開かれます。 これはコードと読みやすさを少し複雑にしますが、ラムダはこれをかなりうまくやってくれます。
結果は次のとおりです。
- 均質テスト;
- テストの次の各ステップは、次のステップを正式に説明します。これは、コンパイラーとIDEのサポートによって完全に補完されます。これは、静的メソッドを使用する場合(少なくともテストの初期バージョン)には達成できません。
- テストの初期化とその後の検証は、抽象テストによって実行され、特定のテストは、実際に元のユニットと直接やり取りしなくても、単純にチェックを記述します。