保持重構後程式運行的正確性是重構前的第一考量
因此在重構前一定要先建立對應的測試系統,這裡使用 junit 來建立 android project 的 test project
1. 為了避免測試碼和程式碼的混淆,另外建立 android test project 來存放 test code
Eclipse -> File -> New -> Other -> Android Test Project
2. 在 android test project 根據要測試的 class 建立相應的 package 和 test class
target class -> GameMap.java in com.foxx.map package
Create test class -> GameMapTest.java in com.foxx.map.test package
3. 開始在 test class 撰寫 test case
Target class(GameMap.java)如下,我的任務是了解這個class,並將它重構
/** * Map of game */ public class GameMap implements IGameMap { private static final int MAP_UNKNOW = 0; private static final int MAP_BARRIER = 1; private static final int MAP_BLANK = -1; private static final int RAW_MAP_DATA_ONE = 1; private static final int BASIC_MAP_INDEX = 0; protected int mMapWidth; protected int mMapHeight; protected Bitmap mMapBitmap = null; protected Canvas mCanvas; private Context mContext; public boolean mIsMerging = true; public boolean mIsConvertDisable = false; public GameMap(Context context) { mContext = context; } @Override public void createMap(Map map) { ArrayList<Map> maps = new ArrayList<Map>(); maps.add(map); createMaps(maps); } @Override public void createMaps(ArrayList<Map> maps) { if (maps.isEmpty() || mIsConvertDisable) return; if (maps.get(0) == null) return; //obtain raw map's width and height mMapWidth = maps.get(0).getWidth(); mMapHeight = maps.get(0).getHeight(); // initial empty map if (mMapBitmap == null) { if (mMapWidth <= 0 || mMapHeight <= 0) return; mMapBitmap = Bitmap.createBitmap(mMapWidth, mMapHeight, Bitmap.Config.ARGB_4444); mCanvas = new Canvas(mMapBitmap); } mIsMerging = true; drawMapsWithColor(maps); mIsMerging = false; } private void drawMapsWithColor(ArrayList<Map> maps) { for (Map map : maps) { int[] coloredMap = TransforByteArrayToColoredMap(map); if (coloredMap != null) { mCanvas.drawBitmap(coloredMap, 0, map.getWidth(), 0, 0, map.getWidth(), map.getHeight(), true, null); } } } // change byte raw data into a color raw data private int[] TransforByteArrayToColoredMap(Map map) { byte[] prePixels = map.getData(); int length = prePixels.length; int[] colorData = null; try { colorData = new int[length]; } catch(OutOfMemoryError E){ mIsConvertDisable = true; return null; } // the color should be discussed int colorUnknow = mContext.getResources().getColor( R.color.map_unknow); int colorBarrier = mContext.getResources().getColor( R.color.map_barrier); int colorBlank = mContext.getResources().getColor( R.color.map_space); int colorOfVirtualWall = mContext.getResources().getColor( R.color.vw_paint); int colorOfTouchZone = mContext.getResources().getColor( R.color.tz_save); int mapIdx = map.getIndex(); if (mapIdx == BASIC_MAP_INDEX) { // basic map for (int i = 0; i < length; ++i) { switch (prePixels[i]) { case MAP_UNKNOW: colorData[newArrayPtr(i)] = colorUnknow; break; case MAP_BARRIER: colorData[newArrayPtr(i)] = colorBarrier; break; case MAP_BLANK: colorData[newArrayPtr(i)] = colorBlank; break; } } } else if ((mapIdx >= Fly.VIRTUALWALL_LOWERBOUND) && (mapIdx <= Fly.VIRTUALWALL_UPPERBOUND)) { // map of virtual wall for (int i = 0; i < length; ++i) { if (prePixels[i] == RAW_MAP_DATA_ONE) colorData[newArrayPtr(i)] = colorOfVirtualWall; } } else if (mapIdx < Fly.COVERAGEMAP_MAPINDEX) { // map of clean area for (int i = 0; i < length; ++i) { if (prePixels[i] == RAW_MAP_DATA_ONE) colorData[newArrayPtr(i)] = colorOfTouchZone; } } else { // map of other raw data String color = getColorOfMap(map.getIndex()); for (int i = 0; i < length; ++i) { if (prePixels[i] == RAW_MAP_DATA_ONE) colorData[newArrayPtr(i)] = Color.parseColor(color); } } return colorData; } private String getColorOfMap(int mapIndex) { String color = null; GlobalData globalData = (GlobalData) mContext.getApplicationContext(); String model = globalData.Info().getModel(); MapManager mapManager = new MapManager(mContext); List<MapLayer> supportedMapLayers = mapManager .getSupportedLayers(model); for (MapLayer map : supportedMapLayers) { if (mapIndex == Integer.valueOf(map.getMapIndex())) color = map.getColor(); } return color; } // create new array pointer, startup from left-up side private int newArrayPtr(int oldPtr) { int newPtr; int remainderNum = oldPtr % mMapWidth; int quotientNum = (oldPtr - remainderNum) / mMapWidth; newPtr = mMapWidth * (mMapHeight - quotientNum - 1) + remainderNum; return newPtr; } @Override public Bitmap getBlendingBitmap() { return mMapBitmap; } @Override public boolean isMergingMaps() { return mIsMerging; } @Override public void disableConvert(boolean isDisable) { mIsConvertDisable = isDisable; } @Override public boolean isDisableConvert() { return mIsConvertDisable; } }
GameMap有不少code smell,之後我會一一列舉出來並嘗試修正,但首先必須先建立test class(GameMapTest.java)如下, 選擇 junit version 3,
GameMapTest繼承AndroidTestCase是因為GameMap的建構式只需要傳入Context,並不需要Activity。
反之,如果今天要測試的為Activity就必須繼承ActivityInstrumentationTestCase2。
public class GameMapTest extends AndroidTestCase { private GameMap mGameMap; private Map mMap; @Override protected void setUp() throws Exception { super.setUp(); mMap = new Map(0, 2, 2, new byte[]{0,0,1,1}); mGameMap = new GameMap(getContext()); testCreateMap(); } public void testCreateMap(){ mGameMap.createMap(mMap); assertNotNull(mGameMap); } public void testGetBlendingBitmap(){ Bitmap bitmap = mGameMap.getBlendingBitmap(); assertNotNull(bitmap); } public void testMerginMap(){ boolean isMapMergin = mGameMap.isMergingMaps(); assertEquals(false, isMapMergin); } public void testGetDisableConvert() { boolean isDisableConvert = mGameMap.isDisableConvert(); assertEquals(false, isDisableConvert); } public void testDisableConvert(){ mGameMap.disableConvert(true); boolean isDisableConvert = mGameMap.isDisableConvert(); assertEquals(isDisableConvert, true); } }
繼承 AndroidTestCase 是為了取得 context 物件並傳入 GameMap 建構式,
Test Case 基本上保持一個方法測試一個功能,而 Map mMap 則是測試資料(test fixture),也是為了要驗證重構後結果正確,setUp 方法會在所有測試方法啟動前先執行,
通常會在這裡建立測試資料,寫完 test code 就先跑跑看吧,接上實機執行 test case
如果想驗證測試機制的確可以運行,也可以故意寫錯一些條件,讓junit告訴你,至於 test coverage 要多少,在Refactoring有提到可以運行少部份的測試,主要是驗證重構的結果即可
下篇開始重構 GameMap.java !!!