很多人都知道系統開發的功能測試很重要,但有時候我們省略它並不是認為某個功能不需要測試,而是不知道該功能該如果測試。有時候問題的關鍵是卡在環境整合的複雜性,例如牽涉 web 的 session 或是資料庫存取的問題,就讓人很難隔離這些複雜情況。當然理論上我們可以運用 Mock 的技術來隔離它們,然而現實上,即使運用這些技術可能你可能還是無法解決無法測試的問題。這時候,你需要的通常不會是尋求其它更強大的技術,而是需要在編程核心思維上,做一些小改變,來改善功能的可測性。
最近同人就碰到一個實例,讓我體會到以泛函編程(FP)典範增進功能的可測性。其實,以前遠在美國的 Perter Ho 就曾向包括同人在內的幾位點空間的朋友,分享過相同的觀念,而同人則是藉由最近的實例而對這樣的觀念有更深刻的體驗。
這個實例不是困難的問題,而是在向資料庫查詢時,組合 SQL 語句用到 IN 的條件式,但需要符合的集合卻超過 1000 個,因而導致系統的異常發生。這個問題要解決並不困難,只要每 500 個分批下 SQL 查詢,然後把每個 SQL 查詢到的資料加到查詢結果就可以了。這樣程式會寫成:
int i = 0; do { int fromIdx = i; int toIdx = ((list.size() - i) > QUERY_BATCH_SIZE) ? QUERY_BATCH_SIZE + i : list.size(); List<String> parts = list.subList(fromIdx, toIdx); String sql = sqlExpr.replace(AID_LIST_REPLACEMENT, getListToSql(parts)); Session session = (Session) getEntityManager().getDelegate(); @SuppressWarnings("unchecked") List<Object[]> querylist = session.createSQLQuery(sql).list(); resultList.addAll(querylist); i = toIdx; } while (list.size() - i > 0); return results;
只是上面這段 code 很難被驗證其功能是無誤的。假設以 SQL 向資料庫讀取資料沒問題,那麼讓人關心的問題就會是如何確認分批組合 SQL 語句的演算法會正確無誤,而不是有重覆或遺漏的部分。然而如果要針對它寫測試,那就必須準備有用的測試資料,而且也要面對此功能掛載在主機 context 的問題。我們的系統是採用 Seam Framework 架構,寫測試案例需要承擔一定的負擔及代價,之前沒有人嘗試過,而同人也覺得似乎不該貿然行事。
於是同人試著改變作法,我認為需要測試的並不是讀取資料的環境整合,而是能不能正確沒有重覆和遺漏地分批組合 SQL。所以我應該可以把這段演算抽離出來,並且可以對它做單獨的驗證。同人先用測試案例來表達我對它的願望:
public class PartsOfListTest { @Before public void setUp() throws Exception { } @After public void tearDown() throws Exception { } @Test public void test() { List<String> list = Arrays.asList("1", "2", "3", "4", "5", "6"); List<List<String>> results = new PartsOfList<String>(3).from(list); assertEquals(results.get(0).get(0), "1"); assertEquals(results.get(0).get(1), "2"); assertEquals(results.get(0).get(2), "3"); assertEquals(results.get(1).get(0), "4"); assertEquals(results.get(1).get(1), "5"); assertEquals(results.get(1).get(2), "6"); results = new PartsOfList<String>(2).from(list); assertEquals(results.get(0).get(0), "1"); assertEquals(results.get(0).get(1), "2"); assertEquals(results.get(1).get(0), "3"); assertEquals(results.get(1).get(1), "4"); assertEquals(results.get(2).get(0), "5"); assertEquals(results.get(2).get(1), "6"); } }
從測試案例中找到 PartsOfList<T> 的演算邏輯,用來把 List<T> 分批成 List<List<T>> 的串列。這樣我們就可以把一個很長的串列分成多個較短的串列,然後再進行分批的處理。建構 PartsOfList<T> 需要指定每個子串列的最大空間,並且利用 from() 傳入串列來分成多個子串列。
接下來讓測試案例過關,同人實作 PartsOfList<T> 的內容如下所示:
public class PartsOfList<T> { private int partSize; public PartsOfList(int size) { partSize = size; } public List<List<T>> from(List<T> list) { List<List<T>> results = new ArrayList<List<T>>(); int i = 0; do { int endIndex = (list.size() - i) > partSize ? i + partSize : list.size(); List<T> part = list.subList(i, endIndex); results.add(part); i = endIndex; } while (i < list.size()); return results; } }
然後最前面的程式片段則會改成如下面的內容:
List<List<String>> parts = new PartsOfList<String>(QUERY_BATCH_SIZE).from(list); for (List<String> aids : parts) { String sql = sqlExpr.replace(AID_LIST_REPLACEMENT, getListToSql(aids)); Session session = (Session) getEntityManager().getDelegate(); @SuppressWarnings("unchecked") List<Object[]> querylist = session.createSQLQuery(sql).list(); resultList.addAll(querylist); } return resultList;
我們看到這樣程式碼更專注於它們各自的職責,而且 PartOfList 也可以重覆使用在其它適用的情況,更重要的是程式碼的驗證是可行的,連帶程式碼的可讀性、擴充性、以及彈性都得到加強。這又一次地讓人體會到泛函編程宣告式語意的簡潔有力,搭配與物件導向的思維的整合,無疑是提升系統抽象概念堪稱完美的組合。