1伪造方法和伪造类

在Faking API的上下文中,假方法假类中使用注释@Mock的方法伪类是扩展mockit.MockUp<T>通用基类的任何类,其中T要伪造的类型。下面的示例显示了在伪类中为示例“真实”类 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()”,对于静态代码块可以用$clinit() ,使用方法参见上文

最后,请注意,对于真实类中的all方法和构造函数,如无特殊需要请不要使用伪方法在伪类中不存在相应伪方法的任何此类方法或构造函数将仅保持“原样”,即不会被伪造。

2创造 伪类

给定的假类必须应用于相应的真实类才能生效。通常,这是针对整个测试类或测试套件完成的,但也可以针对单个测试进行。一个:假类可以从任何地方的测试类的内部施加@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();

   ...
}

由于伪类是在测试方法中应用的,因此LoginContextby 的伪造FakeLoginContext仅对特定测试有效。

当实例化的构造函数调用LoginContext执行时,将执行相应的“ $init”伪方法FakeLoginContext同样,在LoginContext#login调用该方法时,将执行相应的伪方法,在这种情况下,该方法将不执行任何操作,因为该方法没有参数且没有void返回类型。这些调用发生在其上的伪类实例是在测试的第一部分中创建的。

2.1可以伪造的方法

到目前为止,我们只有伪造的公共实例方法和伪造的公共实例方法。真实类中的其他几种方法也可以被伪造:具有protected或“包私有”可访问性的 static方法,final方法,native方法方法。更重要的是,static实型类中方法可以用实例伪造方法伪造,反之亦然(带有static伪造物的实例实数方法)。

伪造的方法需要实现,尽管不一定要用字节码(在native方法的情况下)。因此,abstract不能直接伪造方法。

请注意,不需要使用伪造方法public

3伪造未指定的实现类

为了演示此功能,让我们考虑下面的代码进行测试。

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这些实现之一是通过匿名内部类定义的,该内部类从客户端代码完全不可访问(除了使用Reflection)。

给定一个基本类型(可以是interfaceabstract类或任何类型的基本类),我们可以编写一个仅知道该基本类型但所有实现/扩展实现类都被伪造的测试。为此,我们创建一个伪造品,其目标类型仅引用已知的基本类型,并通过类型变量这样做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()都将重定向到伪方法实现,而不管实现接口方法的实际类如何。

4伪造的类初始化器

当一个类在一个或多个静态初始化块中执行某些工作时,我们可能需要将其操作清除,以免干扰测试执行。我们可以为此定义一个特殊的伪造方法,如下所示。

@Test
public void fakingStaticInitializers() {
   new MockUp<ClassWithStaticInitializers>() {
      @Mock
      void $clinit() {
         // Do nothing here (usually). 最好别在这写逻辑,会影响实体类
      }
   };

   ClassWithStaticInitializers.doSomething();
}

当伪造类的静态初始化代码时,必须格外小心。注意,这不仅包括static类中的任何“ ”块,还static包括字段的任何分配(不包括在编译时解析的,不会产生可执行字节码的分配)。由于JVM仅尝试一次初始化一个类,因此还原伪造类的静态初始化代码将无效。因此,如果您伪造尚未由JVM初始化的类的静态初始化,则原始类初始化代码将永远不会在测试运行中执行。这将导致分配给运行时计算的表达式的任何静态字段都保持默认状态下的初始化。 类型的值。

5访问调用上下文

假方法可以有选择地声明一个类型额外的参数mockit.Invocation,前提是它是第 一个参数。对于对相应伪造方法/构造函数的每次实际调用,执行伪造方法时Invocation将自动传递一个对象。

该调用上下文对象提供了几个可以在false方法内使用的getter一种是getInvokedInstance()方法,该方法返回发生调用伪造实例(null如果伪造方法是static)。其他获取器提供对伪造的方法/构造函数的调用次数(包括当前的调用次数),调用参数(如果有的话)和被调用的成员(适当时为java.lang.reflect.Methodjava.lang.reflect.Constructorobject)。下面我们有一个示例测试。

@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进行实际实施

一旦@Mock方法执行,到相应的伪造的方法,任何额外的电话也被重定向到假的方法,导致它的实现要重新输入。但是,如果我们要执行伪造方法的真实实现,则可以对作为伪造方法的第一个参数接收proceed()到的 Invocation对象调用该方法。

下面的示例测试LoginContext使用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;
      }
   }
}

在上面的示例中,即使伪造了某些方法(,也将执行被测内部的所有代码这个例子是人为的。实际上,进行实际实现的能力通常对测试本身(至少不是直接进行 测试)没有用。 LoginContextloginlogout

您可能已经注意到,Invocation#proceed(...)在伪方法中使用时对于相应的实际方法,其行为实际上类似于advice (来自AOP行话)。这是一项强大的功能,可用于某些事物(例如拦截器或装饰器)。

7在测试之间重复使用伪造类

通常,伪类需要在多个测试中使用,甚至需要整体上用于测试运行。一种选择是使用每种测试方法之前运行的测试设置方法对于JUnit,我们使用 @Before批注;与TestNG一起使用@BeforeMethod另一个方法是在测试类设置方法中应用假冒产品:@BeforeClass无论哪种方式,都可以通过在setup方法中简单地实例化伪类来应用伪类。

一旦应用,伪造品将对测试类中的所有测试执行有效。在“之前”方法中应用的伪造品的范围包括测试类可能具有的任何“之后”方法中的代码(@After为JUnit或@AfterMethodTestNG 注释 )。@BeforeClass方法中应用的任何伪造也是如此:它们在任何AfterClass方法执行期间仍然有效但是,一旦最后一个“之后”或“之后类”方法执行完毕,所有伪造品就会自动“撕毁”。

例如,如果我们想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...
}

还可以从基本测试类扩展,该基本测试类可以可选地定义应用一个或多个伪造品的“之前”方法。

8全局伪造

有时,我们可能需要对测试套件的整个范围(所有测试类)应用伪造品,即“全局”伪造品。可以通过设置系统属性,通过外部配置来完成。

fakes系统属性支持以逗号分隔的完全合格的假类名称的列表。如果在JVM启动时指定,则此类类(必须扩展MockUp<T>)将自动应用于整个测试运行。对于所有测试类,在启动伪造类中定义的伪造方法将一直有效,直到测试运行结束。每个伪类都将通过其no-args构造函数实例化,除非在类名称之后提供了一个附加值(例如,如“ -Dfakes=my.fakes.MyFake=anArbitraryStringWithoutCommas”中所示),在这种情况下,伪类应具有一个带有type参数的构造函数String

9应用AOP风格的建议

@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正在执行的。如果需要,可以通过对Invocation对象的类似调用来获取当前调用计数和/或调用参数 当假货被(自动)拆除时,该onTearDown()方法将执行,将测量的时序转储到标准输出中。

12-14 20:47