前言
前一篇文章Testing: 再論測試替身中,已經討論全部的測試替身,也了解測試替身的目的,接著本文再討論其它的依賴注入方式。再論依賴注入
先前介紹建構子依賴注入時,有提到所謂的依賴注入就是透過一個基於介面的入口,我們可以在一個類別中注入一個介面的實作,讓它的方法可以利用這種介面來實現,而依賴注入分為下列幾種方式。我們已經介紹過建構子這個依賴注入方式,接著繼續介紹其它建構子的注入方法。- 建構子(Constructor)
- Setter方法
- 工廠類別(Factory Class)
- 工廠方法(Factory Method)
- 映射(Reflection)
建構子依賴注入
先前介紹的是利用建構子聲明一個參數,將所需的依賴型態傳入,但有時我們不見得會有參數的建構子,若在無參數的建構子時,我們又該如何設計注入?同樣以先前的FileParser物件來說明,先建立一個公開的無參數建構子,程式碼如下。
- FileParserWithoutParameterConstructor物件
public class FileParser {
private IExtensionManager fileExtensionManager;
public FileParser() {
this(new FileExtensionManagerImp());
}
protected FileParser(IExtensionManager fileExtensionManager) {
this.fileExtensionManager = fileExtensionManager;
}
public boolean isValidLogFileName(String fileName) {
return this.fileExtensionManager.isValid(fileName) && FileHelper.basenameWithoutExtension(fileName).length() > 5;
}
}
- StubExtensionManager物件
public class StubExtensionManager implements IExtensionManager {
public boolean shouldExtensionsBeValid;
@Override
public boolean isValid(String fileName) {
return this.shouldExtensionsBeValid;
}
}
- FileParserWithoutParameterConstructorTest測試程式
public class FileParserWithoutParameterConstructorTest {
@Test
public void testNameShorterCharactersIsValidEvenWithSupportedExtension() throws Exception {
// Arrange
StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = true;
FileParserProxy fileParser = new FileParserProxy(fake);
// Act
boolean actualResult = fileParser.isValidLogFileName("short.txt");
// Assert
assertThat(actualResult, is(false));
}
@Test
public void testNameShorterThan6CharactersIsValidEvenWithSupportedExtension() throws Exception {
// Arrange
StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = true;
FileParserProxy fileParser = new FileParserProxy(fake);
// Act
boolean actualResult = fileParser.isValidLogFileName("short_file_name.txt");
// Assert
assertThat(actualResult, is(true));
}
@Test
public void testNameShorterCharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = false;
FileParserProxy fileParser = new FileParserProxy(fake);
// Act
boolean actualResult = fileParser.isValidLogFileName("short.txt");
// Assert
assertThat(actualResult, is(false));
}
@Test
public void testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = false;
FileParserProxy fileParser = new FileParserProxy(fake);
// Act
boolean actualResult = fileParser.isValidLogFileName("short_file_name.txt");
// Assert
assertThat(actualResult, is(false));
}
private class FileParserProxy extends FileParserWithoutParameterConstructor {
public FileParserProxy(IExtensionManager fileExtensionManager) {
super(fileExtensionManager);
}
}
}
從上面可以看出,只是藉由FileParserProxy來繼承一個FileParserWithoutParameterConstructor,並為FileParserProxy設計一個公開且帶有參數的建構子,這樣測試程式則可以使用它並傳入想使用的Stub物件,替換掉我們的依賴物件了,測試也同樣順利完成,如下所示。
Setter依賴注入
Setter依賴注入也很簡單,只要使用一個Setter方法,並且將依賴傳入即可,程式碼如下。- FileParserWithSetterInjection物件
public class FileParserWithSetterInjection {
private IExtensionManager extensions;
public FileParserWithSetterInjection() {
this.extensions = new FileExtensionManagerImp();
}
public void setExtensionManager(IExtensionManager extensions) {
this.extensions = extensions;
}
public boolean isValidLogFileName(String fileName) {
return extensions.isValid(fileName) && FileHelper.basenameWithoutExtension(fileName).length() > 5;
}
}
- FileParserWithSetterInjectionTest測試程式
public class FileParserWithSetterInjectionTest {
@Test
public void testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = true;
// Act
FileParserWithSetterInjection log = new FileParserWithSetterInjection();
log.setExtensionManager(fake);
// Assert
Assert.assertFalse(log.isValidLogFileName("short.ext"));
}
}
從以上程式碼可以看出,我們多加入一個setExtensionManager方法,與建構子依賴注入的方式差不多,測試時就直接將Stub傳入即可,我們同樣順利通過測試囉!
工廠類別依賴注入
工廠類別依賴注入也很簡單,只是將注入的方法移到一個單例(Singleton)的工廠類別物件,再提供一個setInstance的方法就完成,程式碼如下。- FileParserWithFactoryClassInjection物件
public class FileParserWithFactoryClassInjection {
private IExtensionManager fileExtensionManager;
public FileParserWithFactoryClassInjection() {
this.fileExtensionManager = ExtensionManagerFactory.create();
}
public boolean isValidLogFileName(String fileName) {
return this.fileExtensionManager.isValid(fileName) && FileHelper.basenameWithoutExtension(fileName).length() > 5;
}
}
- ExtensionManagerFactory工廠物件
public class ExtensionManagerFactory {
private static IExtensionManager customImplementation = null;
public static IExtensionManager create() {
if (customImplementation != null) {
return customImplementation;
}
return new FileExtensionManagerImp();
}
public static void setInstance(IExtensionManager implementation) {
customImplementation = implementation;
}
}
- FileParserWithFactoryClassInjectionTest測試程式
public class FileParserWithFactoryClassInjectionTest {
@Test
public void testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = true;
ExtensionManagerFactory.setInstance(fake);
// Act
FileParserWithFactoryClassInjection log = new FileParserWithFactoryClassInjection();
// Assert
assertFalse(log.isValidLogFileName("short.exe"));
}
}
看到了?我們透過ExtensionManagerFactory.setInstance(fake)這樣的方式,把Stub給注入進去,接著就能順利通過測試啦!如下圖。
工廠方法依賴注入
工廠方法依賴注入則是在待測物件內建立一個工廠方法,產生相對應的依賴物件,當測試時覆寫該方法替換想要的依賴物件即可,程式如下。- FileParserWithFactoryMethodInjection物件
public class FileParserWithFactoryMethodInjection {
protected IExtensionManager getExtensionManager() {
return new FileExtensionManagerImp();
}
public boolean isValidLogFileName(String fileName) {
return getExtensionManager().isValid(fileName) && FileHelper.basenameWithoutExtension(fileName).length() > 5;
}
}
- FileParserWithFactoryMethodInjectionTest測試程式
public class FileParserWithFactoryMethodInjectionTest {
@Test
public void testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
final StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = true;
FileParserWithFactoryMethodInjection log = new FileParserWithFactoryMethodInjection() {
@Override
protected IExtensionManager getExtensionManager() {
return fake;
}
};
// Act
boolean actualResult = log.isValidLogFileName("shortName.ext");
// Assert
Assert.assertTrue(actualResult);
}
@Test
public void testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtensions() throws Exception {
// Arrange
final StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = true;
FileParserWithFactoryMethodInjectionProxy log = new FileParserWithFactoryMethodInjectionProxy();
log.extensionManager = fake;
// Act
boolean actualResult = log.isValidLogFileName("shortName.ext");
// Assert
Assert.assertTrue(actualResult);
}
class FileParserWithFactoryMethodInjectionProxy extends FileParserWithFactoryMethodInjection {
public IExtensionManager extensionManager;
@Override
protected IExtensionManager getExtensionManager() {
return this.extensionManager;
}
}
}
在測試時有兩種方式,一種你可以另外產生Stub物件,像是上述程式內的FileParserWithFactoryMethodInjectionProxy類別,另一種則是在測試案例中用匿名類別的方式也是能達到同樣目的,如同testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtension測試案例一樣,透過這種方式,我們也能順利通過測試,如下圖。
映射依賴注入
映射依賴注入比較麻煩,就是要利用映射機制來修改原本屬性的存取權限,然後把依賴物件給注入,程式碼如下。- FileParserWithReflectionInjection物件
public class FileParserWithReflectionInjection {
private IExtensionManager extensionManager;
public FileParserWithReflectionInjection() {
this(new FileExtensionManagerImp());
}
protected FileParserWithReflectionInjection(IExtensionManager extensionManager) {
this.extensionManager = extensionManager;
}
public boolean isValidLogFileName(String fileName) {
return this.extensionManager.isValid(fileName) && FileHelper.basenameWithoutExtension(fileName).length() > 5;
}
}
- FileParserWithReflectionInjectionTest測試程式
public class FileParserWithReflectionInjectionTest {
@Test
public void testOverridePrivateModifierOfField() throws Exception {
// Arrange
StubExtensionManager fake = new StubExtensionManager();
fake.shouldExtensionsBeValid = true;
FileParserWithReflectionInjection log = new FileParserWithReflectionInjection();
this.injectToField(log, "extensionManager", fake);
// Act
boolean actualResult = log.isValidLogFileName("validLogFile.ext");
// Assert
assertTrue(actualResult);
}
private void injectToField(Object target, String fieldName, Object dependency) {
try {
Field field = target.getClass().getDeclaredField(fieldName);
if (!field.isAccessible()) {
field.setAccessible(true);
}
field.set(target, dependency);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
從上面的測試程式碼來看,新增了一個injectToField方法,這個方法有三個參數,分別為測試物件、測試物件的屬性名稱,以及依賴物件,再來可以透過getClass().getDeclaredField(“屬性名稱"),取得測試物件的Field,這時就能去修改它的權限,把private設定成public,再透過field.set()方法則能把依賴物件注入了,最後我們也是成功完成測試了,如下圖。
沒有留言:
張貼留言