SoftwareEngineering/ProgramLanguage/Java/UnitTest/JMockit
JMockit Java用の自動テストツールキット
JMockitツールキットでは、 Faking APIは偽の実装を作成するためのサポートを提供します。 一般的に、偽物はクラスのいくつかのメソッドやコンストラクタをターゲットにしていますが、他のほとんどのメソッドやコンストラクタは変更しません。 また、通常、偽装されるクラスはテスト対象のコードベースではなく 、 外部ライブラリに属します。
偽の実装は、電子メールやWebサービスサーバー、複雑なライブラリなどの外部コンポーネントやリソースに依存するテストで特に役立ちます。 テストクラスから直接ではなく、再利用可能なテストインフラストラクチャコンポーネントから適用されることがよくあります。 特に、Faking APIとMocking APIの両方を同じテストクラスで使用することは、誤用を強く示唆しているため、疑念を持って表示する必要があります。
実際の実装を偽のものに置き換えることは、それらの依存関係を使用するコードに対して完全に透過的であり、単一のテスト、単一のテストクラス内のすべてのテスト、またはテスト全体の実行のオン/オフを切り替えることができます。
Faking APIの文脈では、 偽のメソッドは、 @Mock注釈を付けられた偽のクラスのメソッド@Mock 。 偽のクラスは、 mockit.MockUp<T>ジェネリック基本クラスを拡張する任意のクラスです。 ここでTは偽造される型です。 以下の例は、 "real"クラスjavax.security.auth.login.LoginContextの例として、偽のクラスで定義されたいくつかの偽のメソッドを示しています 。
public final class FakeLoginContext extends MockUp<LoginContext> { @Mock public void $init(String name, CallbackHandler callback) { assertEquals("test", name); assertNotNull(callback); } @Mock public void login() {} @Mock public Subject getSubject() { return null; } }
フェイククラスが実クラスに適用されるとき、後者は、対応する偽メソッドが仮のクラスで定義されているように、対応する仮メソッドの実装に一時的に置き換えられたメソッドとコンストラクタの実装を取得します。 言い換えれば、実際のクラスは、偽のクラスを適用したテスト期間中、「偽装された」状態になります。 そのメソッドは、テスト実行中に呼び出しを受け取るたびに応答します。 実行時に、実際には、偽装されたメソッド/コンストラクタの実行がインターセプトされ、対応する偽のメソッドにリダイレクトされ、元の呼び出し元に実行され、(例外/エラーがスローされない限り)返されます実際には別の方法が実行されました。 通常、「呼び出し元」クラスはテスト対象クラスであり、偽装クラスは依存関係です。
それぞれの@Mockメソッドは、ターゲットの実クラスに同じシグネチャを持つ対応する「実メソッド/コンストラクタ」を持っていなければなりません。 メソッドの場合、シグネチャはメソッド名とパラメータで構成されます。 コンストラクタの場合、それは単なるパラメータであり、偽のメソッドは特別な名前 " $init "を持ちます。 指定された実クラスまたはスーパークラス( java.lang.Objectを除く)のいずれかで、指定されたfakeメソッドで一致する実メソッドまたはコンストラクタが見つからない場合、テストで偽のクラスを適用しようとするとIllegalArgumentExceptionがスローされます。 この例外は、実際のメソッドの名前を変更するなど、実際のクラスのリファクタリングによって発生する可能性があるので、その理由を理解することが重要です。
最後に、実際のクラスのすべてのメソッドとコンストラクタに対して偽のメソッドを持つ必要はないことに注意してください。 対応する偽のメソッドが偽のクラスに存在しないそのようなメソッドまたはコンストラクタは、そのまま "そのまま"、つまり偽装されません。
与えられた偽のクラスは、その効果を得るために、対応する実クラスに適用されなければなりません。 これは通常、テストクラス全体またはテストスイートに対して行われますが、個々のテストでも実行できます。 @BeforeClass 、 @BeforeClassメソッド、 @BeforeMethod / @Before / @BeforeEachメソッド(TestNG / JUnit 4 / JUnit 5)、または@Testメソッドからテストクラス内のどこからでも適用できます。 偽のクラスが適用されると、偽装されたメソッドと実クラスのコンストラクタのすべての実行が自動的に対応する偽のメソッドにリダイレクトされます。
上記のFakeLoginContextフェイククラスを適用するには、単純にインスタンス化します。
@Test public void applyingAFakeClass() throws Exception { new FakeLoginContext()); // Inside an application class which creates a suitable CallbackHandler: new LoginContext("test", callbackHandler).login(); ... }
偽のクラスはテストメソッドの内部で適用されるので、 FakeLoginContext LoginContextの偽装はその特定のテストのためにのみ有効です。
LoginContextをインスタンス化するコンストラクタ呼び出しが実行されると、 FakeLoginContext内の対応する " $init " fakeメソッドが実行されます。 同様に、 LoginContext#loginメソッドが呼び出されると、対応する偽のメソッドが実行されます。 この場合、メソッドにはパラメータとvoid戻り値の型がないため、この場合は何も行いません。 これらの呼び出しが発生する偽クラスインスタンスは、テストの最初の部分で作成されたものです。
これまでは、publicインスタンスのメソッドをpublicインスタンスのfakeメソッドで偽装していました。 実際には、 private 、 protectedまたは「パッケージプライベート」のアクセシビリティ、 staticメソッド、 finalメソッド、およびnativeメソッドを持つメソッドは、実際のクラスの他の種類のメソッドを偽造することができます。 さらに、実際のクラスのstaticメソッドは、 インスタンスの偽のメソッドによって偽装できます。 逆もまた同様です( static偽のインスタンスの実際のメソッド)。
偽装されるメソッドは、必ずしもバイトコード( nativeメソッドの場合)ではなく、実装を持つ必要があります。 したがって、 abstractメソッドを直接偽装することはできません。
偽のメソッドはpublicする必要はありません。
この機能を実証するには、テスト対象の次のコードを検討してください。
public interface Service { int doSomething(); } final class ServiceImpl implements Service { public int doSomething() { return 1; } } public final class TestedUnit { private final Service service1 = new ServiceImpl(); private final Service service2 = new Service() { public int doSomething() { return 2; } }; public int businessOperation() { return service1.doSomething() + service2.doSomething(); } }
テストするメソッドbusinessOperation()は、別のインタフェースServiceを実装するクラスを使用します。 これらの実装の1つは、クライアントコードから完全にアクセスできない(Reflectionの使用を除く)匿名の内部クラスによって定義されます。
基本型( interface 、 abstractクラス、または基本クラスのいずれか)を指定すると、基本型のみを認識し、すべての実装/拡張実装クラスが偽装されたテストを書くことができます。 これを行うために、ターゲットタイプが既知の基本タイプのみを参照し、 タイプ変数を使用して擬似を作成します 。 JVMによってすでにロードされている実装クラスは偽装されるだけでなく、後でテストを実行する際にJVMによってロードされる追加のクラスも偽装されます。 この能力は以下に説明されています。
@Test public <T extends Service> void fakingImplementationClassesFromAGivenBaseType() { new MockUp<T>() { @Mock int doSomething() { return 7; } }; int result = new TestedUnit().businessOperation(); assertEquals(14, result); }
上記のテストでは、 Service#doSomething()メソッドを実装している実際のクラスに関係なく、 Service#doSomething()を実装するメソッドへのすべての呼び出しは、偽メソッド実装にリダイレクトされます。
クラスが1つまたは複数の静的初期化ブロックで何らかの作業を実行する場合、テスト実行を妨げないようにスタブする必要があります。 以下に示すように、特別な偽の方法を定義することができます。
@Test public void fakingStaticInitializers() { new MockUp<ClassWithStaticInitializers>() { @Mock void $clinit() { // Do nothing here (usually). } }; ClassWithStaticInitializers.doSomething(); }
クラスの静的初期化コードが偽装されている場合は、特別な注意が必要です。 これには、クラス内の「 static 」ブロックだけでなく、 staticフィールドへの割り当て(コンパイル時に解決され、実行可能バイトコードを生成しないものを除く)も含まれます。 JVMはクラスを一度しか初期化しようとしないので、偽装されたクラスの静的初期化コードを復元することは効果がありません。 したがって、まだJVMで初期化されていないクラスの静的初期化を偽造すると、元のクラス初期化コードはテスト実行中に決して実行されません。 これにより、実行時に計算された式で割り当てられた静的フィールドは、その型のデフォルト値で初期化されたままになります 。
fakeメソッドは、 最初のパラメータであれば、オプションでmockit.Invocation型の余分なパラメータを宣言することができます。 対応する偽装メソッド/コンストラクタへの実際の呼び出しごとに、偽装メソッドが実行されるときにInvocationオブジェクトが自動的に渡されます。
この呼び出しコンテキストオブジェクトは、偽のメソッドの内部で使用できるいくつかのgetterを提供します。 1つはgetInvokedInstance()メソッドで、呼び出しが発生した偽装されたインスタンス(偽装されたメソッドがstatic場合はnull getInvokedInstance()を返します。 他のゲッターは、偽装されたメソッド/コンストラクター、呼び出し引数(存在する場合)、呼び出されたメンバー( java.lang.reflect.Methodまたはjava.lang.reflect.Constructorオブジェクト)への呼び出し数(現在のものを含む) 、 適切に)。 以下は、サンプルテストです。
@Test public void accessingTheFakedInstanceInFakeMethods() throws Exception { Subject testSubject = new Subject(); new MockUp<LoginContext>() { @Mock void $init(Invocation invocation, String name, Subject subject) { assertNotNull(name); assertSame(testSubject, subject); // Gets the invoked instance. LoginContext loginContext = invocation.getInvokedInstance(); // Verifies that this is the first invocation. assertEquals(1, invocation.getInvocationCount()); // Forces setting of private Subject field, since no setter is available. Deencapsulation.setField(loginContext, subject); } @Mock void login(Invocation invocation) { // Gets the invoked instance. LoginContext loginContext = invocation.getInvokedInstance(); // getSubject() returns null until the subject is authenticated. assertNull(loginContext.getSubject()); // Private field set to true when login succeeds. Deencapsulation.setField(loginContext, "loginSucceeded", true); } @Mock void logout(Invocation invocation) { // Gets the invoked instance. LoginContext loginContext = invocation.getInvokedInstance(); assertSame(testSubject, loginContext.getSubject()); } }; LoginContext theFakedInstance = new LoginContext("test", testSubject); theFakedInstance.login(); theFakedInstance.logout(); }
@Mockメソッドが実行されると、対応するfakeメソッドへの追加呼び出しもfakeメソッドにリダイレクトされ、その実装が再入力されます。 しかし、擬似メソッドの実際の実装を実行したい場合、偽メソッドの最初のパラメータとして受け取ったInvocationオブジェクトのproceed()メソッドを呼び出すことができます。
以下のサンプル・テストでは、指定されていないconfigurationを使用して、正常に作成されたLoginContextオブジェクトを作成します(作成時に偽装することはありません)。
@Test public void proceedIntoRealImplementationsOfFakedMethods() throws Exception { // Create objects used by the code under test: LoginContext loginContext = new LoginContext("test", null, null, configuration); // Apply fakes: ProceedingFakeLoginContext fakeInstance = new ProceedingFakeLoginContext(); // Exercise the code under test: assertNull(loginContext.getSubject()); loginContext.login(); assertNotNull(loginContext.getSubject()); assertTrue(fakeInstance.loggedIn); fakeInstance.ignoreLogout = true; loginContext.logout(); // first entry: do nothing assertTrue(fakeInstance.loggedIn); fakeInstance.ignoreLogout = false; loginContext.logout(); // second entry: execute real implementation assertFalse(fakeInstance.loggedIn); } static final class ProceedingFakeLoginContext extends MockUp<LoginContext> { boolean ignoreLogout; boolean loggedIn; @Mock void login(Invocation inv) throws LoginException { try { inv.proceed(); // executes the real code of the faked method loggedIn = true; } finally { // This is here just to show that arbitrary actions can be taken inside the // fake, before and/or after the real method gets executed. LoginContext lc = inv.getInvokedInstance(); System.out.println("Login attempted for " + lc.getSubject()); } } @Mock void logout(Invocation inv) throws LoginException { // We can choose to proceed into the real implementation or not. if (!ignoreLogout) { inv.proceed(); loggedIn = false; } } }
上記の例では、いくつかのメソッド( loginとlogout )が偽装されていても、テストされたLoginContextクラス内のすべてのコードが実行されlogin 。 この例は考案されています。 実際には、実際の実装に進む能力は、通常、少なくとも直接的にではなく、それ自体をテストするためには通常役に立ちません。
偽のメソッドでInvocation#proceed(...)を使用すると、対応する実際のメソッドのアドバイス (AOP専門用語)のように効果的に動作することに気付かれるかもしれません。 これは強力な能力であり、特定のもの(インターセプターやデコレーターと考える)に役立ちます。
mockit.Invocationクラスで使用できるすべてのメソッドの詳細については、 APIドキュメントを参照してください。
しばしば、偽のクラスは、複数のテストで使用する必要があります。 あるいは、テスト全体を実行するために適用する必要があります。 1つのオプションは、各テストメソッドの前に実行されるテストセットアップメソッドを使用することです。 JUnitでは@Beforeアノテーションを使用します。 TestNGでは@BeforeMethodです。 もう一つは、テストクラス設定メソッドの内部に偽を適用することです: @BeforeClass 。 いずれにしても、偽のクラスは、設定メソッド内でインスタンス化するだけで適用されます。
適用されると、テストクラスのすべてのテストを実行するための偽物が有効になります。 "before"メソッドに適用される偽のスコープには、テストクラスが持つ可能性のある "after"メソッドのコードが含まれます(JUnit @AfterMethod 、TestNGでは@AfterMethodで@Afterれています)。 @BeforeClassメソッドで適用された@BeforeClass場合も同じです: @BeforeClassメソッドの実行中もAfterClassです。 最後の "after"メソッドまたは "after class"メソッドの実行が終了すると、すべてのFakeは自動的に "破棄"されます。
たとえば、 LoginContextの関連するテストでLoginContextクラスを偽のクラスで偽装したければ、JUnitテストクラスには次のメソッドがあります。
public class MyTestClass { @BeforeClass public static void applySharedFakes() { new MockUp<LoginContext>() { // shared @Mock's here... }; } // test methods that will share the fakes applied above... }
1つまたは複数の偽を適用する「前」メソッドを任意に定義できる基本テストクラスから拡張することもできます。
場合によっては、テストスイートのすべてのスコープ(すべてのテストクラス)、つまり「グローバルな」フェイクに偽物を適用する必要があります。 このような偽物は、試運転全体にわたって有効なままです。 これは、テストコードまたは外部設定によって行うことができます。
テストスイートに偽物を適用するには、TestNG @BeforeSuiteメソッドまたはJUnit Suiteクラスを使用できます。 次の例は、グローバルな偽のアプリケーションを使用したJUnit 4テストスイート構成を示しています。
@RunWith(Suite.class) @Suite.SuiteClasses({MyFirstTest.class, MySecondTest.class}) public final class TestSuite { @BeforeClass public static void applyGlobalFake() { new FakeLogging(); } }
この例では、 FakeLogging偽クラスを適用します。 その偽の実装方法は、テストスイートの最後のテストが実行された直後まで有効です。
fakesシステムプロパティは、完全修飾の偽のクラス名のコンマ区切りのリストをサポートしています。 JVMの起動時に指定すると、そのようなクラス( MockUp<T> 拡張する必要があります)がテストの実行全体に自動的に適用されます。 起動時の偽のクラスで定義された偽のメソッドは、すべてのテストクラスに対して、テストが終了するまで有効です。 クラス名の後ろに追加の値が与えられていない限り(たとえば、 "-Dfakes = my.fakes.MyFake = anArbitraryStringWithoutCommas"のように)、偽のクラスはその引数なしのコンストラクタによってインスタンス化されます。 String型の1つのパラメータを持つコンストラクタを持つ。
システムプロパティは、標準の " -D "コマンドラインパラメータを介してJVMに渡すことができます。 Maven / Gradle / Ant / etc。 ビルドスクリプトにはシステムプロパティを指定する独自の方法がありますので、詳細についてはドキュメントを確認してください。
偽のクラスに現れる特別な@Mockメソッドがもう一つあります: " $advice "メソッドです。 定義されている場合、この偽のメソッドは、ターゲットクラス(または、ベースクラスから指定されていないクラスに偽を適用する場合)の各メソッドの実行を処理します。 通常の偽のメソッドとは異なり、このメソッドは特定のシグネチャと戻り値の型を必要とします: Object $advice(Invocation) 。
デモンストレーションでは、各メソッドの元のコードを実行しながら、テスト実行中にあるクラスのすべてのメソッドの実行時間を測定したいとします。
public final class MethodTiming extends MockUp<Object> { private final Map<Method, Long> methodTimes = new HashMap<>(); public MethodTiming(Class<?> targetClass) { super(targetClass); } MethodTiming(String className) throws ClassNotFoundException { super(Class.forName(className)); } @Mock public Object $advice(Invocation invocation) { long timeBefore = System.nanoTime(); try { return invocation.proceed(); } finally { long timeAfter = System.nanoTime(); long dt = timeAfter - timeBefore; Method executedMethod = invocation.getInvokedMember(); Long dtUntilLastExecution = methodTimes.get(executedMethod); Long dtUntilNow = dtUntilLastExecution == null ? dt : dtUntilLastExecution + dt; methodTimes.put(executedMethod, dtUntilNow); } } @Override protected void onTearDown() { System.out.println("\nTotal timings for methods in " + targetType + " (ms)"); for (Entry<Method, Long> methodAndTime : methodTimes.entrySet()) { Method method = methodAndTime.getKey(); long dtNanos = methodAndTime.getValue(); long dtMillis = dtNanos / 1000000L; System.out.println("\t" + method + " = " + dtMillis); } } }
上記の偽のクラスは、テストの中、 "before"メソッド、 "before class"メソッド、または " -Dfakes=testUtils.MethodTiming=my.application.AppClass "を設定することによってテスト全体に適用することができます。 これは、与えられたクラスのすべてのメソッドのすべての実行の実行時間を合計します。 $adviceメソッドの実装に示されているように、実行中のjava.lang.reflect.Methodを取得できjava.lang.reflect.Method 。 必要に応じて、 Invocationオブジェクトへの同様の呼び出しによって、現在の呼び出しカウントおよび/または呼び出し引数を取得できます。 偽物が(自動的に)分解されると、 onTearDown()メソッドが実行され、測定されたタイミングが標準出力にダンプされます。