2011-11-14

在 Android 使用 UI 單元測試(UI Unit Test)

從昨天的範例 在 Android 使用單元測試(Unit Test) 繼續,Android 的 Unit test 除了可以作為程式邏輯的單元測試外,也可以用來測試 UI。

稍微修改昨天的測試標的,增加答對與答錯次數的紀錄,再用單元測試來做檢查。

BitOperatorActivity
public class BitOperatorActivity extends Activity implements OnClickListener {

    private static final String TAG = "BitOperatorActivity";
    private static final String BIT_OR = "|";
    private static final String BIT_NOT = "^";
    private static final String BIT_AND = "&";
    private List<String> operatorList = new ArrayList<String>();
    private String aOperand;
    private String bOperand;
    private String operator;
    private TextView aTv;
    private TextView bTv;
    private TextView operatorTv;
    private EditText cOperand;
    private TextView sTv;
    private TextView fTv;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        this.aTv = (TextView) this.findViewById(R.id.a);
        this.bTv = (TextView) this.findViewById(R.id.b);
        this.operatorTv = (TextView) this.findViewById(R.id.operator);
        this.cOperand = (EditText) this.findViewById(R.id.c);
        this.sTv = (TextView) this.findViewById(R.id.successCnt);
        this.fTv = (TextView) this.findViewById(R.id.failureCnt);
        ((Button) this.findViewById(R.id.btn)).setOnClickListener(this);
        this.operatorList.add(BitOperatorActivity.BIT_AND);
        this.operatorList.add(BitOperatorActivity.BIT_NOT);
        this.operatorList.add(BitOperatorActivity.BIT_OR);
    }

    @Override
    protected void onResume() {
        super.onResume();
        this.newGame();
    }

    private void newGame() {
        int a = (int) (100 * Math.random());
        int b = (int) (100 * Math.random());
        this.aOperand = Integer.toBinaryString(a);
        this.bOperand = Integer.toBinaryString(b);
        Collections.shuffle(this.operatorList);
        this.operator = this.operatorList.get(0);
        this.aTv.setText(this.aOperand);
        this.bTv.setText(this.bOperand);
        this.operatorTv.setText(this.operator);
        this.cOperand.setText("");
    }

    /** for test only */
    public String getOperator() {
        return this.operator;
    }

    public String bitOperate(String a, String b) {
        int aInt = Integer.parseInt(a, 2);
        int bInt = Integer.parseInt(b, 2);
        int cInt;
        if (this.operator.equals(BIT_AND)) {
            Log.d(TAG, "And operator");
            cInt = aInt & bInt;
        }
        else if (this.operator.equals(BIT_NOT)) {
            Log.d(TAG, "Not operator");
            cInt = aInt ^ bInt;
        }
        else if (this.operator.equals(BIT_OR)) {
            Log.d(TAG, "Or operator");
            cInt = aInt | bInt;
        }
        else {
            Log.e(TAG, "Unknown operator: " + this.operator);
            Toast.makeText(this, "程式錯誤!", Toast.LENGTH_LONG).show();
            return null;
        }
        return Integer.toString(cInt, 2);
    }

    @Override
    public void onClick(View v) {
        Log.d(TAG, "onclick...");
        switch (v.getId()) {
        case R.id.btn:
            String answer = this.cOperand.getText().toString();
            String rightAnswer = this.bitOperate(this.aOperand, this.bOperand);
            Log.d(TAG, "Right answer: " + rightAnswer);
            if (rightAnswer.equals(answer)) {
                Log.d(TAG, "You are right!");
                Toast.makeText(this, "你答對了!", Toast.LENGTH_LONG).show();
                this.sTv.setText(String.valueOf(Integer.parseInt(this.sTv.getText().toString()) + 1));
                this.newGame();
            }
            else {
                Log.d(TAG, "You are wrong! Your answer is " + answer + ".");
                Toast.makeText(this, "哇咧!你答錯了,再試一次!", Toast.LENGTH_LONG).show();
                this.fTv.setText(String.valueOf(Integer.parseInt(this.fTv.getText().toString()) + 1));
            }
            break;
        }
    }
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  >
    <LinearLayout 
      android:orientation="horizontal"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:gravity="center_horizontal"
      >
        <TextView  
            android:id="@+id/success"
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="成功:"
          />
        <TextView  
            android:id="@+id/successCnt"
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="0"
          />
        <TextView  
            android:id="@+id/failure"
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text=", 失敗:"
          />
        <TextView  
            android:id="@+id/failureCnt"
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="0"
          />
    </LinearLayout>
    <TextView  
        android:id="@+id/a"
      android:layout_width="60px" 
      android:layout_height="wrap_content" 
      android:layout_gravity="center_horizontal"
      android:gravity="right"
      />
    <TextView  
        android:id="@+id/operator"
      android:layout_width="wrap_content" 
      android:layout_height="wrap_content" 
      android:layout_gravity="center_horizontal"
      android:gravity="center"
      android:text="&"
      />
    <TextView  
        android:id="@+id/b"
      android:layout_width="60px" 
      android:layout_height="wrap_content" 
      android:layout_gravity="center_horizontal"
      android:gravity="right"
      />
    <TextView  
      android:layout_width="wrap_content" 
      android:layout_height="30px" 
      android:layout_gravity="center_horizontal"
      android:gravity="center"
      android:text="等於"
      />
    <EditText  
        android:id="@+id/c"
      android:layout_width="100px" 
      android:layout_height="wrap_content" 
      android:layout_gravity="center_horizontal"
      android:gravity="right"
      />
    <Button  
        android:id="@+id/btn"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:gravity="center"
      android:text="GO"
      />
</LinearLayout>

再來就是兩個 UI 單元測試的重點:

Activity.runOnUiThread()

UI 的動作要在 UI Thread 裡執行,不管是改值或觸發動作,不可以在單元測試的 Thread 裡執行,但是取值就沒關係。

Instrumentation.waitForIdleSync()

runOnUiThread() 不會 hold 住目前的 thread,所以會立即往下執行,因此得呼叫 waitForIdleSync() 來等待 UI Thread 執行完成,否則會發生不可預期的狀況。

BitOperatorActivityTest
public class BitOperatorActivityTest extends
        ActivityInstrumentationTestCase2<BitOperatorActivity> {

    private TextView aTv;
    private TextView bTv;
    private TextView operatorTv;
    private TextView sTv;
    private TextView fTv;
    private EditText cOperand;
    private Button goBtn;

    public BitOperatorActivityTest() {
        super("idv.neil.bitOperator", BitOperatorActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        // 是用測試標的的 idv.neil.bitOperator.R,不是測試 project 的 R
        this.aTv = (TextView) this.getActivity().findViewById(R.id.a);
        this.bTv = (TextView) this.getActivity().findViewById(R.id.b);
        this.operatorTv = (TextView) this.getActivity().findViewById(
                R.id.operator);
        this.sTv = (TextView) this.getActivity().findViewById(R.id.successCnt);
        this.fTv = (TextView) this.getActivity().findViewById(R.id.failureCnt);
        this.cOperand = (EditText) this.getActivity().findViewById(R.id.c);
        this.goBtn = (Button) this.getActivity().findViewById(R.id.btn);
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    public void testBitOperate() {
        BitOperatorActivity act = this.getActivity();
        int aInt = 48;
        int bInt = 16;
        String a = Integer.toBinaryString(aInt);
        String b = Integer.toBinaryString(bInt);
        String c = act.bitOperate(a, b);
        int cInt = Integer.parseInt(c, 2);
        String operator = act.getOperator();
        assertNotNull(operator);
        int answer = 0;
        if ("&".equals(operator)) {
            answer = aInt & bInt;
        }
        else if ("|".equals(operator)) {
            answer = aInt | bInt;
        }
        else if ("^".equals(operator)) {
            answer = aInt ^ bInt;
        }
        else {
            fail("Wrong operator - " + operator);
        }
        assertEquals(answer, cInt);
    }

    public void testCount() {
        // 答對
        this.clickButton(this.getAnswer());
        // 執行完成了,取值判斷
        assertEquals("1", this.sTv.getText().toString());
        assertEquals("0", this.fTv.getText().toString());

        // 答錯
        this.clickButton(this.getAnswer() + 1);
        // 執行完成了,取值判斷
        assertEquals("1", this.sTv.getText().toString());
        assertEquals("1", this.fTv.getText().toString());

        // 又答錯
        this.clickButton(this.getAnswer() - 1);
        // 執行完成了,取值判斷
        assertEquals("1", this.sTv.getText().toString());
        assertEquals("2", this.fTv.getText().toString());

        // 總算答對
        this.clickButton(this.getAnswer());
        // 執行完成了,取值判斷
        assertEquals("2", this.sTv.getText().toString());
        assertEquals("2", this.fTv.getText().toString());
    }

    private int getAnswer() {
        String a = this.aTv.getText().toString();
        String b = this.bTv.getText().toString();
        int aInt = Integer.parseInt(a, 2);
        int bInt = Integer.parseInt(b, 2);
        String operator = this.operatorTv.getText().toString();
        final int c;
        if ("&".equals(operator)) {
            c = aInt & bInt;
        }
        else if ("|".equals(operator)) {
            c = aInt | bInt;
        }
        else if ("^".equals(operator)) {
            c = aInt ^ bInt;
        }
        else {
            fail("Wrong operator - " + operator);
            c = 0;
        }
        return c;
    }

    private void clickButton(final int c) {
        // UI 的動作要在 UI Thread 裡執行
        // 不管是改值或觸發動作
        this.getActivity().runOnUiThread(new Runnable() {

            @Override
            public void run() {
                cOperand.setText(Integer.toString(c, 2));
                goBtn.performClick();
            }
        });
        // 等待 UI Thread 執行完成
        this.getInstrumentation().waitForIdleSync();
    }
}
完工。

Android Emulator Log:


Console Log,今天的 console log 比較短是因為 Android Emulator 已經啟動了:

沒有留言:

張貼留言