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(); ... }
由於偽類是在測試方法中應用的,因此LoginContext
by 的偽造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)。
給定一個基本類型(可以是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()
都將重定向到偽方法實現,而不管實現接口方法的實際類如何。
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
)。其他獲取器提供對偽造的方法/構造函數的調用次數(包括當前的調用次數),調用參數(如果有的話)和被調用的成員(適當時為a java.lang.reflect.Method
或java.lang.reflect.Constructor
object)。下面我們有一個示例測試。
@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; } } }
在上面的示例中,即使偽造了某些方法(和),也將執行被測類內部的所有代碼。這個例子是人為的。實際上,進行實際實現的能力通常對測試本身(至少不是直接進行 測試)沒有用。 LoginContext
login
logout
您可能已經注意到,Invocation#proceed(...)
在偽方法中使用時,對於相應的實際方法,其行為實際上類似於advice (來自AOP行話)。這是一項強大的功能,可用於某些事物(例如攔截器或裝飾器)。
7在測試之間重復使用偽造類
通常,偽類需要在多個測試中使用,甚至需要整體上用於測試運行。一種選擇是使用在每種測試方法之前運行的測試設置方法。對於JUnit,我們使用 @Before
批注;與TestNG一起使用@BeforeMethod
。另一個方法是在測試類設置方法中應用假冒產品:@BeforeClass
。無論哪種方式,都可以通過在setup方法中簡單地實例化偽類來應用偽類。
一旦應用,偽造品將對測試類中的所有測試執行有效。在“之前”方法中應用的偽造品的范圍包括測試類可能具有的任何“之后”方法中的代碼(@After
為JUnit或@AfterMethod
TestNG 注釋 )。@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()
方法將執行,將測量的時序轉儲到標准輸出中。