測試對象序列化

來源:互聯網
上載者:User
測試對象序列化

容易被遺漏的重要測試

文檔選項
<tr
valign="top"><td width="8"><img alt="" height="1" width="8"
src="//www.ibm.com/i/c.gif"/></td><td width="16"><img alt="" width="16"
height="16" src="//www.ibm.com/i/c.gif"/></td><td class="small"
width="122"><p><span class="ast">未顯示需要 JavaScript
的文檔選項</span></p></td></tr>


列印本頁

將此頁作為電子郵件發送

層級: 初級

Elliotte Harold (elharo@metalab.unc.edu), 副教授, Polytechnic University

2006 年 7 月 06 日

即使最傑出的開發人員有時也會忘記測試對象序列化,但那並不能作為您犯下同一錯誤的借口。在這篇文章中,Elliotte Rusty Harold 將解釋對對象序列化進行單元測試的重要性,並為您展示一些應牢記的測試。


測試驅動的開發的總體原則之一就是應測試一個類發行的所有介面。如果客戶機能夠調用方法或訪問欄位,那麼就測試它。但在 Java 語言中,許多類都有一個發行的介面容易被遺漏:通過類執行個體產生的序列化對象。有時這些類顯式實現 Serializable。而有時則是直接從超類繼承這一特性。在任何一種情況下,您都應該測試其序列化形式。本文將介紹幾種測試對象序列化的方法。

測試序列化

對序列化來說,測試極其重要,因為序列化非常非常容易出錯。在修複 bug
或最佳化類時,非常容易破壞所有已有序列化對象。如果您在更改代碼時未考慮序列化,幾乎可以肯定您必將破壞原有對象。若您正在為任何形式的持久性儲存使用串
行化,那麼這將是一個嚴重的 bug。即便僅為流程間的瞬時訊息傳遞(如在 RMI
中)使用對象序列化,更改序列化格式也會使那些各類的版本不完全相同的系統無法順利交換資料。

告訴每一個人。將此提交到:

 


Digg

Slashdot

幸運的是,若您謹慎對待序列化問題,在處理類時通常可以避免不相容的更改。Java 語言提供了多種方法,可維護一個類的不同版本之間的相容性,包括:

  • serialVersionUID
  • transient 修飾符
  • readObject()writeObject()
  • writeReplace()readResolve()
  • serialPersistentFields

對於這些解決方案來說,最大的問題就在於程式員未使用它們。當您將精力集中在修複
bug、添加特性或解決效能問題時,往往不會停下來思考您的更改對序列化造成的影響。然而序列化是一個涉及範圍極廣的問題 ——
跨越一個系統的多個不同層。幾乎所有更改都會涉及對序列化有某種影響的一個類的執行個體欄位。這正是單元測試發揮作用的時機。在本文後續各節中,我將為您展示
一些簡單的單元測試,這些單元測試能確保您不會不經意地更改可序列化類的串列格式。



回頁首

我能否將其序列化?

通常您編寫的第一個序列化測試就是用於驗證序列化是否可行的測試。即使一個類實現了 Serializable,依然不能保證它能夠序列化。例如,如果一個可序列化的容器(如 ArrayList)包含一個不可序列化的對象(如 Socket),則在您嘗試序列化此容器時,將拋出 NotSerializableException

通常,對此測試,您只需在 ByteArrayOutputStream 上寫入資料。若未拋出任何異常,測試即通過。如果您願意,還可測試一些已寫入的輸出。例如,清單 1 所示程式碼片段用於測試 Jaxen 的 BaseXPath 類是否可序列化:

清單 1. 此類是否可序列化?

  
public void testIsSerializable()
throws JaxenException, IOException {

BaseXPath path = new BaseXPath("//foo", new DocumentNavigator());
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(path);
oos.close();
assertTrue(out.toByteArray().length > 0);

}


回頁首

測試序列化形式

接下來,您想要編寫一個測試,不僅要驗證輸出得到了顯示,還要驗證輸出是正確的。您可通過兩種方式完成這一任務:

  • 反序列化對象,並將其與原始對象相比較。
  • 逐位元組地將其與參考 .ser 檔案相比較。

我通常會從第一種選擇入手,因為它還提供了一個反序列化的簡單測試,而且編碼和實現相對來說比較容易。例如,清單 2 所示程式碼片段將測試 Jaxen 的 SimpleVariableContext 類是否可寫入並在之後重新讀回:

清單 2. 反序列化對象,並將其與原始對象相比較

  
public void testRoundTripSerialization()
throws IOException, ClassNotFoundException, UnresolvableException {

// construct test object
SimpleVariableContext original = new SimpleVariableContext();
original.setVariableValue("s", "String Value");
original.setVariableValue("x", new Double(3.1415292));
original.setVariableValue("b", Boolean.TRUE);

// serialize
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(original);
oos.close();

//deserialize
byte[] pickled = out.toByteArray();
InputStream in = new ByteArrayInputStream(pickled);
ObjectInputStream ois = new ObjectInputStream(in);
Object o = ois.readObject();
SimpleVariableContext copy = (SimpleVariableContext) o;

// test the result
assertEquals("String Value", copy.getVariableValue("", "", "s"));
assertEquals(Double.valueOf(3.1415292), copy.getVariableValue("", "", "x"));
assertEquals(Boolean.TRUE, copy.getVariableValue("", "", "b"));
assertEquals("", "");

}

讓我們再試一次……

在測試代碼基礎中那些此前從未測試過的部分時,幾乎總是會發現 bug,對象序列化也是這樣。在我第一次運行清單 2 中的測試時,測試失敗了,輸出結果如清單 3 所示:

清單 3. 不可序列化

 
java.io.NotSerializableException:
org.jaxen.QualifiedName
at
java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1075)
at
java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:291)
at java.util.HashMap.writeObject(HashMap.java:984)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)

at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

at java.lang.reflect.Method.invoke(Method.java:585)
at
java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:890)
at
java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1333)
at
java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1284)

at
java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1073)
at
java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1369)
at
java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1341)
at
java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1284)

at
java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1073)
at
java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:291)
at
org.jaxen.test.SimpleVariableContextTest.testRoundTripSerialization
(SimpleVariableContextTest.java:90)

這表明,SimpleVariableContext 包含一個對 QualifiedName 對象的引用,QualifiedName 類未標記為 Serializable。我為 QualifiedName 的類簽名添加了 implements Serializable,這一次測試順利通過。

注意,此測試實際上並未驗證序列化格式是否正確 —— 只是驗證出對象能夠來迴轉換。為測試正確性,您需要產生一些參考檔案,以便與類的所有未來版本的輸出相比較。



回頁首

測試反序列化

通常,您不能依賴預設序列化格式來保持類的不同版本間的檔案格式相容性。您必須使用 serialPersistentFieldsreadObject()writeObject() 方法和/或 transient 修飾符,通過各種方式進行定製。如果您確實對類的序列化格式做出了不相容的更改,應相應更改 serialVersionUID 欄位,以指出您這樣做了。

正常情況下,您不會過分關注序列化對象的詳細結構。而只是關注最初使用的那種格式隨著類的發展得到了維護。一旦類基本上具備了恰當的形式,即可寫入
一些類的序列化執行個體,並儲存在隨後可將其作為參考使用的位置處。(您很可能確實希望多多少少地考慮如何序列化才能確保足夠的靈活性,以便應對未來的發
展。)

編寫序列化執行個體的程式是臨時代碼,只需使用一次。實際上,您根本就不應該多次運行這段代碼,因為您不希望獲得序列化格式中的任何意外更改。例如,清單 4 展示了用於序列化 Jaxen 的 SimpleVariableContext 類的程式:

清單 4. 寫入序列化執行個體的程式

import org.jaxen.*;
import java.io.*;

public class MakeSerFiles {

public static void main(String[] args) throws IOException {

OutputStream fout = new FileOutputStream("xml/simplevariablecontext.ser");
ObjectOutputStream out = new ObjectOutputStream(fout);

SimpleVariableContext context = new SimpleVariableContext();
context.setVariableValue("s", "String Value");
context.setVariableValue("x", new Double(3.1415292));
context.setVariableValue("b", Boolean.TRUE);

out.writeObject(context);
out.flush();
out.close();

}

}

您只需將一個序列化對象寫入檔案 —— 而且只需一次。這是您希望儲存的檔案,而不是用於寫入的代碼。清單 5 展示了 Jaxen 的 SimpleVariableContext 類的相容性測試:

清單 5. 確保檔案格式未被更改

  
public void testSerializationFormatHasNotChanged()
throws IOException, ClassNotFoundException, UnresolvableException {

//deserialize
InputStream in = new FileInputStream("xml/simplevariablecontext.ser");
ObjectInputStream ois = new ObjectInputStream(in);
Object o = ois.readObject();
SimpleVariableContext context = (SimpleVariableContext) o;

// test the result
assertEquals("String Value", context.getVariableValue("", "", "s"));
assertEquals(Double.valueOf(3.1415292), context.getVariableValue("",
"", "x"));
assertEquals(Boolean.TRUE, context.getVariableValue("", "", "b"));
assertEquals("", "");

}


回頁首

測試不可串列性

預設情況下,類通常是可序列化的。例如,java.lang.Throwablejava.awt.Component 的任何子類都會從其祖先繼承可串列性。在某些情況下,這也是您希望的結果,但並非總是如此。有的時候,序列化可能會成為安全性漏洞,使惡意程式員能夠在不調用建構函式或 setter 方法的情況下建立對象,從而規避了您小心翼翼地在類中構建的所有約束性檢查。

若您希望類可序列化,就需要測試它,這與您需要測試一個直接實現了 Serializable 的類相同。如果您不希望類可序列化,則應重寫 writeObject()readObject(),使兩者均拋出 NotSerializableException,隨後您也需要對其進行測試。

此類測試的實現方法與其他任何 JUnit 異常測試相似。只需在應拋出異常的語句兩端包圍一個 try 塊即可,隨後緊接欲拋出異常的語句之後添加一條 fail() 語句。如果願意,您還可在 catch 中作出一些關於所拋出異常的斷言。例如,清單 6 驗證了 FunctionContext 是不可序列化的:

清單 6. 測試 FunctionContext 是不可序列化的

  
public void testSerializeFunctionContext()
throws JaxenException, IOException {

DOMXPath xpath = new DOMXPath("/root/child");
FunctionContext context = xpath.getFunctionContext();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(out);
try {
oout.writeObject(context);
fail("serialized function context");
}
catch (NotSerializableException ex) {
assertNotNull(ex.getMessage());
}

}

Java 5 和 JUnit 4 使異常測試更為輕鬆。只需在 @Test 注釋中聲明所需異常即可,如清單 7 所示:

清單 7. 帶有注釋的異常測試

@Test(expected=NotSerializableException.class) public
void testSerializeFunctionContext()
throws JaxenException, IOException {

DOMXPath xpath = new DOMXPath("/root/child");
FunctionContext context = xpath.getFunctionContext();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(out);
oout.writeObject(context);

}


回頁首

結束語

序列化格式可以說是代碼基礎中最脆弱、健壯性最差的部分。有的時候,似乎只要以奇異的眼神盯著它,它就會被破壞。單元測試和測試驅動的開發這些出色的工具使您可以信心十足地管理此類脆弱系統 —— 但只有在您確實使用了這些工具時,它們才能發揮作用。

若您關注對象序列化,特別是希望為長期持久性儲存使用序列化對象時,就必須對序列化進行測試。不要假設您的 Java 代碼所做的一切都是正確的
——
它很可能會出錯!如果您將序列化測試作為測試套件的固定部分,則維護長期相容性就會更輕鬆。您花費在對象序列化單元測試上的時間將為您帶來成倍的回報,此
後調試時您能節省的時間將數倍於投入時間。

參考資料

學習

  • 您可以參閱本文在 developerWorks 全球網站上的 英文原文 。
  • “利用 Ant 和 JUnit 進行漸進式開發”(Malcolm Davis,developerWorks,2000 年 11 月):介紹 Java 平台上的單元測試。
  • “揭開極端編程的神秘面紗: 測試驅動的編程”(Roy Miller,developerWorks,2003 年 4 月):介紹關於測試驅動編程的一切,更重要的是 —— 測試驅動編程與什麼無關。

  • Keeping critters out of your code”(David Carew,Sandeep Desai,Anthony Young-Garner,developerWorks,2003 年 6 月):介紹伺服器端應用伺服器環境的單元測試。
  • “JUnit 4 搶先看”(Elliotte Rusty Harold,developerWorks,2005 年 9 月):介紹 JUnit 4 中基於注釋的全新架構,需要 Java 5 或更新版本。
  • Java I/O,第 2 版(Elliotte Rusty Harold;O'Reilly,2006 年 5 月):深入討論對象序列化的新版圖書。

  • Pragmatic Unit Testing
    (Dave Thomas 和 Andy Hunt;Pragmatic Programmer,2003 年 9 月):單元測試 Java 代碼的完整介紹。
  • Java 技術專區:數百篇關於 Java 編程各個方面的文章。

獲得產品和技術

  • JUnit:影響您的測試。

討論

  • developerWorks
    blogs:加入 developerWorks 社區。

關於作者

Elliotte
Rusty Harold 來自新奧爾良, 現在他還定期回老家喝一碗美味的秋葵湯。不過目前,他和妻子 Beth 定居在紐約臨近布魯克林的
Prospect Heights,同住的還有他的貓咪 Charm(取自夸克)和 Marjorie(取自他嶽母的名字)。他是
Polytechnic 大學電腦科學系的一名副教授,講授 Java 和物件導向編程。他的 Cafe au Lait Web 網站已經成為 Internet 上最流行的獨立 Java 網站之一,它的姊妹網站 Cafe con Leche 是最流行的 XML 網站之一。他編寫的圖書包括 Effective XMLProcessing XML with JavaJava Network ProgrammingJava I/O
目前,他在從事處理 XML 的 XOM API、Jaxen XPath 引擎和 Jester 測試覆蓋工具的開發工作。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.