2016年12月7日 星期三

Testing: 再論依賴注入

前言

前一篇文章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()方法則能把依賴物件注入了,最後我們也是成功完成測試了,如下圖。


小結

我們對於測試替身以及依賴注入的介紹夠多了,為何使用測試替身和依賴注入?原因在於需要隔離測試目標,這樣才能模擬出所有情境,並且測試到應有的行為。但目前我們介紹測試替身都是使用手動建置,手動建置有它的成本,但為何要先介紹手動建置?個人認為應該先理解原理,再去尋求更快捷的方法,所以接下來我們就可以利用其他便利的框架,為我們的單元測試提供更多的服務囉!

Source Code

Github: https://github.com/xavier0507/UnitTestSample.git

沒有留言:

張貼留言