保持重構後程式運行的正確性是重構前的第一考量

因此在重構前一定要先建立對應的測試系統,這裡使用 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 !!!