JMockit Java言語のための自動テストツールキット

フェイク (実装の差し替え)

  1. Fake methods and fake classes
  2. Applying fake classes
    1. Kinds of methods which can be faked
  3. Faking unspecified implementation classes
  4. Faking class initializers
  1. Accessing the invocation context
  2. Proceeding into the real implementation
  3. Reusing fakes between tests
  4. Global fakes
  5. Applying AOP-style advice

JMockitでは,偽の実装への差し替えを可能にするフェイクAPIを提供しています. ふつう,クラス内のいくつかのメソッドやコンストラクタのみを差し替えて,その他のメソッドやコンストラクタは元のままにします.また,ふつう差し替えたいクラスは外部ライブラリにあるもので,テスト対象のコードではないことが多いでしょう.

偽の実装は,メールやWebサービス,サーバ,複雑なライブラリといった外部コンポーネントやリソースに依存するテストで特に役立つでしょう. 多くの場合,実装の差し替えは,テストクラスで直接行うのではなく.再利用可能な共通的なテストコンポーネントで行われます.テストクラス内でフェイクAPIとモック化APIの両方を使っている場合,誤った使い方である可能性が高いため,注意する必要があります.

実装の差し替えは,その実装を使おうとするコードに対して完全に透過的に機能します.また,テストケース,テストクラス,あるいはテスト全体,といった範囲でオンオフを切り替えることが可能です.

1 Fake methods and fake classes

In the context of the Faking API, a fake method is any method in a fake class that gets annotated with @Mock. A fake class is any class extending the mockit.MockUp<T> generic base class, where T is the type to be faked. The example below shows several fake methods defined in a fake class for our example "real" class, 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; }
}

When a fake class is applied to a real class, the latter gets the implementation of those methods and constructors which have corresponding fake methods temporarily replaced with the implementations of the matching fake methods, as defined in the fake class. In other words, the real class becomes "faked" for the duration of the test which applied the fake class. Its methods will respond accordingly whenever they receive invocations during test execution. At runtime, what really happens is that the execution of a faked method/constructor is intercepted and redirected to the corresponding fake method, which then executes and returns (unless an exception/error is thrown) to the original caller, without this one noticing that a different method was actually executed. Normally, the "caller" class is one being tested, while the faked class is a dependency.

Each @Mock method must have a corresponding "real method/constructor" with the same signature in the targeted real class. For a method, the signature consists of the method name and parameters; for a constructor, it's just the parameters, with the fake method having the special name "$init". If a matching real method/constructor cannot be found for a given fake method, either in the specified real class or in its super-classes (excluding java.lang.Object), an IllegalArgumentException is thrown when the test attempts to apply the fake class. Notice this exception can be caused by a refactoring in the real class (such as renaming the real method), so it's important to understand why it happens.

Finally, notice there is no need to have fake methods for all methods and constructors in a real class. Any such method or constructor for which no corresponding fake method exists in the fake class will simply stay "as is", that is, it won't be faked.

2 Applying fake classes

A given fake class must be applied to a corresponding real class to have any effect. This is usually done for a whole test class or test suite, but can also be done for an individual test. Fakes can be applied from anywhere inside a test class: a @BeforeClass method, a @BeforeMethod / @Before / @BeforeEach method (TestNG / JUnit 4 / JUnit 5), or from a @Test method. Once a fake class is applied, all executions of the faked methods and constructors of the real class get automatically redirected to the corresponding fake methods.

To apply the FakeLoginContext fake class above, we simply instantiate it:

@Test
public void applyingAFakeClass() throws Exception {
   new FakeLoginContext());

   // Inside an application class which creates a suitable CallbackHandler:
   new LoginContext("test", callbackHandler).login();

   ...
}

Since the fake class is applied inside a test method, the faking of LoginContext by FakeLoginContext will be in effect only for that particular test.

When the constructor invocation that instantiates LoginContext executes, the corresponding "$init" fake method in FakeLoginContext will be executed. Similarly, when the LoginContext#login method is called, the corresponding fake method will be executed, which in this case will do nothing since the method has no parameters and void return type. The fake class instance on which these invocations occur is the one created in the first part of the test.

2.1 Kinds of methods which can be faked

So far, we have only faked public instance methods with public instance fake methods. Several other kinds of methods in a real class can also be faked: methods with protected or "package-private" accessibility, static methods, final methods, and native methods. Even more, a static method in the real class can be faked by an instance fake method, and vice-versa (an instance real method with a static fake).

Methods to be faked need to have an implementation, though not necessarily in bytecode (in the case of native methods). Therefore, an abstract method cannot be faked directly.

Note that fake methods don't need to be public.

3 Faking unspecified implementation classes

To demonstrate this feature, lets consider the following code under test.

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

The method we want to test, businessOperation(), uses classes that implement a separate interface, Service. One of these implementations is defined through an anonymous inner class, which is completely inaccessible (except for the use of Reflection) from client code.

Given a base type (be it an interface, an abstract class, or any sort of base class), we can write a test which only knows about the base type but where all implementing/extending implementation classes get faked. To do so, we create a fake whose target type refers only to the known base type, and does so through a type variable. Not only will implementation classes already loaded by the JVM get faked, but also any additional classes that happen to get loaded by the JVM during later test execution. This ability is demonstrated below.

@Test
public <T extends Service> void fakingImplementationClassesFromAGivenBaseType() {
   new MockUp<T>() {
      @Mock int doSomething() { return 7; }
   };

   int result = new TestedUnit().businessOperation();

   assertEquals(14, result);
}

In the test above, all invocations to methods implementing Service#doSomething() will be redirected to the fake method implementation, regardless of the actual class implementing the interface method.

4 Faking class initializers

When a class performs some work in one or more static initialization blocks, we may need to stub it out so it doesn't interfere with test execution. We can define a special fake method for that, as shown below.

@Test
public void fakingStaticInitializers() {
   new MockUp<ClassWithStaticInitializers>() {
      @Mock
      void $clinit() {
         // Do nothing here (usually).
      }
   };

   ClassWithStaticInitializers.doSomething();
}

Special care must be taken when the static initialization code of a class is faked. Note that this includes not only any "static" blocks in the class, but also any assignments to static fields (excluding those resolved at compile time, which do not produce executable bytecode). Since the JVM only attempts to initialize a class once, restoring the static initialization code of a faked class will have no effect. So, if you fake away the static initialization of a class that hasn't been initialized by the JVM yet, the original class initialization code will never be executed in the test run. This will cause any static fields that are assigned with expressions computed at runtime to instead remain initialized with the default values for their types.

5 Accessing the invocation context

A fake method can optionally declare an extra parameter of type mockit.Invocation, provided it is the first parameter. For each actual invocation to the corresponding faked method/constructor, an Invocation object will be automatically passed in when the fake method is executed.

This invocation context object provides several getters which can be used inside the fake method. One is the getInvokedInstance() method, which returns the faked instance on which the invocation occurred (null if the faked method is static). Other getters provide the number of invocations (including the current one) to the faked method/constructor, the invocation arguments (if any), and the invoked member (a java.lang.reflect.Method or java.lang.reflect.Constructor object, as appropriate). Below we have an example test.

@Test
public void accessingTheFakedInstanceInFakeMethods() throws Exception {
   new MockUp<LoginContext>() {
      Subject testSubject;

      @Mock
      void $init(Invocation invocation, String name, Subject subject) {
         assertNotNull(name);
         assertNotNull(subject);

         // Verifies this is the first invocation.
         assertEquals(1, invocation.getInvocationCount());
      }

      @Mock
      void login(Invocation invocation) {
         // Gets the invoked instance.
         LoginContext loginContext = invocation.getInvokedInstance();
         assertNull(loginContext.getSubject()); // null until subject is authenticated
         testSubject = new Subject();
      }

      @Mock
      void logout() { testSubject = null; }

      @Mock
      Subject getSubject() { return testSubject; }
   };

   LoginContext theFakedInstance = new LoginContext("test", new Subject());
   theFakedInstance.login();
   assertSame(testSubject, theFakedInstance.getSubject();
   theFakedInstance.logout();
   assertNull(theFakedInstance.getSubject();
}

6 Proceeding into the real implementation

Once a @Mock method is executing, any additional calls to the corresponding faked method are also redirected to the fake method, causing its implementation to be re-entered. If, however, we want to execute the real implementation of the faked method, we can call the proceed() method on the Invocation object received as the first parameter to the fake method.

The example test below exercises a LoginContext object created normally (without any faking in effect at creation time), using an unspecified configuration.

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

In the example above, all the code inside the tested LoginContext class will get executed, even though some methods (login and logout) are faked. This example is contrived; in practice, the ability to proceed into real implementations would not normally be useful for testing per se, not directly at least.

You may have noticed that use of Invocation#proceed(...) in a fake method effectively behaves like advice (from AOP jargon) for the corresponding real method. This is a powerful ability that can be useful for certain things (think of an interceptor or decorator).

7 Reusing fakes between tests

Often, a fake class needs to be used throughout multiple tests, or even applied for the test run as a whole. One option is to use test setup methods that run before each test method; with JUnit, we use the @Before annotation; with TestNG, it's @BeforeMethod. Another is to apply fakes inside of a test class setup method: @BeforeClass. Either way, the fake class is applied by simply instantiating it inside the setup method.

Once applied, a fake will remain in effect for the execution of all tests in the test class. The scope of a fake applied in a "before" method includes the code in any "after" methods the test class may have (annotated with @After for JUnit or @AfterMethod for TestNG). The same goes for any fakes applied in a @BeforeClass method: they will still be in effect during the execution of any AfterClass methods. Once the last "after" or "after class" method finish being executed, though, all fakes get automatically "torn down".

For example, if we wanted to fake the LoginContext class with a fake class for a bunch of related tests, we would have the following methods in a JUnit test class:

public class MyTestClass
{
   @BeforeClass
   public static void applySharedFakes() {
      new MockUp<LoginContext>() {
         // shared @Mock's here...
      };
   }

   // test methods that will share the fakes applied above...
}

It is also possible to extend from a base test class, which may optionally define "before" methods that apply one or more fakes.

8 Global fakes

Sometimes, we may need to apply a fake for the entire scope of a test suite (all of its test classes), ie, a "global" fake. This can be done through external configuration, by setting a system property.

The fakes system property supports a comma-separated list of fully qualified fake class names. If specified at JVM startup time, any such class (which must extend MockUp<T>) will be automatically applied for the whole test run. The fake methods defined in startup fake classes will remain in effect until the end of the test run, for all test classes. Each fake class will be instantiated through its no-args constructor, unless an additional value was provided after the class name (for example, as in "-Dfakes=my.fakes.MyFake=anArbitraryStringWithoutCommas"), in which case the fake class should have a constructor with one parameter of type String.

9 Applying AOP-style advice

There is one more special @Mock method that can appear in a fake class: the "$advice" method. If defined, this fake method will handle executions of each and every method in the target class (or classes, when applying the fake over unspecified classes from a base type). Differently from regular fake methods, this one needs to have a particular signature and return type: Object $advice(Invocation).

For demonstration, lets say we want to measure the execution times of all methods in a given class during test execution, while still executing the original code of each method.

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

The fake class above can be applied inside a test, in a "before" method, in a "before class" method, or even for the entire test run by setting "-Dfakes=testUtils.MethodTiming=my.application.AppClass". It will add up the execution times for all executions of all methods in a given class. As shown in the implementation of the $advice method, it can obtain the java.lang.reflect.Method that is being executed. If desired, the current invocation count and/or the invocation arguments could be obtained through similar calls to the Invocation object. When the fake is (automatically) torn down, the onTearDown() method gets executed, dumping measured timings to standard output.