保持重構後程式運行的正確性是重構前的第一考量
因此在重構前一定要先建立對應的測試系統,這裡使用 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 !!!