at java.io.FileInputStream.(FileInputStream.java:106)
at java.io.FileInputStream.(FileInputStream.java:66)
at java.io.FileReader.(FileReader.java:41)
at com.foo.bar.config.ServerConfigAgent.parseFile(ServerConfigAgent.java:1593)
at com.foo.bar.config.ServerConfigAgent.parseConfigFile(ServerConfigAgent.java:1720)
at com.foo.bar.config.ServerConfigAgent.parseConfigFile(ServerConfigAgent.java:1712)
at com.foo.bar.config.ServerConfigAgent.readServerConf(ServerConfigAgent.java:1581)
at com.foo.bar.ServerConfigFactory.initServerConfig(ServerConfigFactory.java:38)
at com.foo.bar.util.HibernateUtil.setupDatabaseProperties(HibernateUtil.java:207)
at com.foo.bar.util.HibernateUtil.doStart(HibernateUtil.java:135)
at com.foo.bar.util.HibernateUtil.(HibernateUtil.java:125)
看起來只要找到server.conf文件就可以了,但這種方式讓我有些不爽。僅僅才編寫了一個簡單的測試用例,就暴露出了代碼中的一個問題。正如HibernateUtil的名字所建議的,它僅僅關心數據庫信息,而這些信息應該都由database.properties文件提供,為什么還需要訪問用以配置服務端啟動信息的server.conf文件呢?這里似乎暗示代碼散發出壞味道了:當你感覺到自己如同在讀一本偵探小說,不斷地問“為什么”的時候,這就意味著代碼是糟糕的。如果我再多花一些時間完整地看一下ServerConfigFactory、HibernateUtil和ServerConfigAgent這些類,大概能夠找到讓HibernateUtil直接使用database.properties的方法吧。但那個時候的我已經心煩意亂了,始終沒法讓程序啟動。順便說一句,這里有一種處理它的臨時方案,這把武器就是AspectJ:
void around():
call(public static void com.foo.bar.ServerConfigFactory.initServerConfig()){
System.out.println("bypassing com.foo.bar.ServerConfigFactory.initServerConfig");
}
讓我用直白一點的方式為不了解AspectJ的讀者介紹一下以上代碼的含義吧:當運行時準備調用ServerConfigFactory.initServerConfig()方法,讓它打印出一條信息,然后跳過該方法的執行并直接返回。聽起來好像是某種hack,但它大大降低了開銷。遺留系統中充斥著問題與謎團,與其打交道的每一時刻都得采取些策略。眼下,從客戶滿意度這點看來,對我來說最有意義的事情就是修復這個資源管理系統中的缺陷,并改善它的性能。其它方面的代碼整理并不是我的當前目標,但我已記住了這個問題,我決定之后再回來處理ServerMain中的問題。
接下來,在每一處HibernateUtil需要讀取server.conf文件的地方,我都強制讓它從database.properties中進行讀取。
String around():call(public String com.foo.bar.config.ServerConfig.getJDBCUrl()){
// code omitted, reading from database.properties
}
String around():call(public String com.foo.bar.config.ServerConfig.getDBUser()){
// code omitted, reading from database.properties
}
接下來的工作你大概能猜到了,如果使用臨時方案會比較方便又顯得自然,那就使用它。而如果有現成的mock對象可以使用,那么就重用它們。舉例來說,TestServerMain.main()方法在某一時刻會產生如下錯誤:
- Factory name: java:comp/env/hibernate/SessionFactory
- JNDI InitialContext properties:{}
- Could not bind factory to JNDI
javax.naming.NoInitialContextException: Need to specify class name in environment
or system property, or as an applet
parameter, or in an application resource file: java.naming.factory.initial
at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:645)
at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:288)
這是由于JBoss命名服務未啟動造成的,雖然我也可以使用相同的hack技術作為臨時方案,但InitialContext是一個龐大的Javax接口,它包含了數量眾多的方法,我可不想把每個方法都用hack方式給補完,那實在太冗長了。我很快發現Spring里已經包含了一個mock的SimpleNamingContext類了,那么就把它放到測試里去試試:
SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
builder.bind(“java:comp/env/hibernate/SessionFactory”,sessionFactory);
builder.activate();
經過幾次反復的修改后,我終于能夠成功地運行TestServerMain.main()方法了。它比起ServerMain來說要簡單許多,不僅mock了許多JBoss的服務,而且完全避免了集群管理的麻煩。
創建構造塊
TestServerMain連接了某個真實的數據庫,而遺留系統往往會在存儲過程、甚至是觸發器中隱藏了各種出人意料的邏輯?;趯ο到y整體情況的考慮,我認為在當前狀況下試圖理解數據庫中的所有奧秘、并以此創建一個偽造的數據庫是一個不明智的選擇,因此我決定仍然在測試用例中訪問真實的數據庫。
這些測試用例必須保證它們能夠重復運行,以確保我對產品代碼所做的任何小改動都能夠通過測試。每一次運行,測試用例都會在數據庫中創建資源與請求。與單元測試的習慣作法不同的是,有時你并不希望在每次運行之后對測試用例所創建的各種數據進行清理。我們目前為止所做的測試與重構練習更像是一次實地考查的探索——通過對遺留系統進行測試的方式來學習它的功能。為了確保一切功能都像預期一樣工作,你也許需要在數據庫中檢查由測試用例所創建的數據,或者需要在運行時使用這些數據。這就意味著測試用例每一次運行時都要在數據庫中創建新的唯一實體,以避免與其它測試用例相沖突。最好能編寫一些實用工具類以方便地創建這些實體。
原文轉自:http://www.infoq.com/cn/articles/refactoring-legacy-applications