通过JUnit进行Android单元测试
要了解android单元测试,首先必须了解junit
什么是 JUnit ?
JUnit是采用测试驱动开发的方式,也就是说在开发前先写好测试代码,主要用来说明被测试的代码会被如何使用,错误处理等;然后开始写代码,并在测试代码中逐步测试这些代码,直到最后在测试代码中完全通过。
现简要说JUnit的4大功能
- 管理测试用例。修改了哪些代码,这些代码的修改会对哪些部分有影响,通过JUnit将这次的修改做个完整测试。这也就JUnit中所谓的TestSuite。
- 定义测试代码。这也就是JUnit中所谓的TestCase,根据源代码的测试需要定义每个TestCase,并将TestCase添加到相应的TestSuite方便管理。
- 定义测试环境。在TestCase测试前会先调用“环境”配置,在测试中使用,当然也可以在测试用例中直接定义测试环境。
- 检测测试结果。对于每种正常、异常情况下的测试,运行结果是什么、结果是否是我们预期的等都需要有个明确的定义,JUnit在这方面提供了强大的功能。
以上部分与我们平常使用IDE调试的过程是完全一样的,只不过是增加了测试用例管理、测试结果检测等功能,提高了单元的效率,保证了单元测试的完整性,明确了单元测试的目标。
一个 JUnit 测试包含以下元素:
开发代码部分 | 测试代码部分 | 测试工具部分 |
待测试类 A | 通过扩展 TestCase 或者构造 TestSuit 方法 编写测试类 B |
一个测试运行器(TestRunner)R,可以选择图形界面或文本界面 |
操作步骤:
将 B 通过命令行方式或图形界面选择方式传递给 R,R 自动运行测试,并显示结果。
首先看下junit测试类库和android中单元测试类库:
SDK | 功能说明 |
junit.framework | JUnit测试框架 |
junit.runner | 实用工具类支持JUnit测试框架 |
android.test | Android 对JUnit测试框架的扩展包 |
android.test.mock | Android的一些辅助类 |
android.test.suitebuilder | 实用工具类,支持类的测试运行 |
在这些包中最为重要的是: junit.framework、 android.test,其中前者是JUnit的核心包,后者是Andoid SDK在Junit.framework的基础上扩展出来的包,我们将重点解析这2个包。
junit.framework:
- TestSuit:
TestSuite
是测试用例的集合; - TestCase:定义运行多个测试用例;
- TestResult:收集一个测试案例的结果,测试结果分为失败和错误,如果未能预计的断言就是失败,错误就像一个ArrayIndexOutOfBoundsException异常而导致的无法预料的问题;
- TestFailure:测试失败时捕获的异常;
- Assert:断言的方法集,当断言失败时显示信息;
TestCase与TestSuite之间的关系,有些类似于图元对象与容器对象之间的关系
android.test包:
JUnit TestCase类
继承自JUnit的TestCase,不能使用Instrumentation框架。但这些类包含访问系统对象(如Context)的方法。使用Context,你可以浏览资源,文件,数据库等等。基类是AndroidTestCase,一般常见的是它的子类,和特定组件关联。
子类有:
l ApplicationTestCase——测试整个应用程序的类。它允许你注入一个模拟的Context到应用程序中,在应用程序启动之前初始化测试参数,并在应用程序结束之后销毁之前检查应用程序。
l ProviderTestCase2——测试单个ContentProvider的类。因为它要求使用MockContentResolver,并注入一个IsolatedContext,因此Provider的测试是与OS孤立的。
l ServiceTestCase——测试单个Service的类。你可以注入一个模拟的Context或模拟的Application(或者两者),或者让Android为你提供Context和MockApplication。
Instrumentation TestCase类
继承自JUnit TestCase类,并可以使用Instrumentation框架,用于测试Activity。使用Instrumentation,Android可以向程序发送事件来自动进行UI测试,并可以精确控制Activity的启动,监测Activity生命周期的状态。
基类是InstrumentationTestCase。它的所有子类都能发送按键或触摸事件给UI。子类还可以注入一个模拟的Intent。
子类有:
l ActivityTestCase——Activity测试类的基类。
l SingleLaunchActivityTestCase——测试单个Activity的类。它能触发一次setup()和tearDown(),而不是每个方法调用时都触发。如果你的测试方法都是针对同一个Activity的话,那就使用它吧。
l SyncBaseInstrumentation——测试Content Provider同步性的类。它使用Instrumentation在启动测试同步性之前取消已经存在的同步对象。
l ActivityUnitTestCase——对单个Activity进行单一测试的类。使用它,你可以注入模拟的Context或Application,或者两者。它用于对Activity进行单元测试。
不同于其它的Instrumentation类,这个测试类不能注入模拟的Intent。
l ActivityInstrumentationTestCase2——在正常的系统环境中测试单个Activity的类。你不能注入一个模拟的Context,但你可以注入一个模拟的Intent。另外,你还可以在UI线程(应用程序的主线程)运行测试方法,并且可以给应用程序UI发送按键及触摸事件。
下面找了几个例子:
首先看junit,然后在看android test:
junit:
待测试类A:
package cn.edu.wtu.junit; public class Calcuator { public double add(double n1, double n2) { return n1 + n1; } }
测试代码B:扩展testcase
package cn.edu.wtu.junit; import junit.framework.TestCase; public class TestCalcuator extends TestCase { public void testAdd(){ Calcuator calcuator=new Calcuator(); double result=calcuator.add(1,2); assertEquals(3,result,0); } }
testsuit 测试容器: package cn.edu.wtu.junit; import junit.framework.Test; import junit.framework.TestSuite; import junit.textui.TestRunner; public class TestAll extends TestSuite { public static Test suite() { TestSuite suite = new TestSuite("TestSuite Test"); suite.addTestSuite(TestCalcuator.class); suite.addTestSuite(TestCalcuator2.class); return suite; } public static void main(String args[]){ // 命令行输出 测试工具 一个测试运行器 TestRunner.run(suite()); } }
run on junit 图形界面显示:
run java application:控制台输出:
android test :
首先看下非instrumentation框架测试:
测试代码:
package aexp.junit; import android.test.AndroidTestCase; import android.util.Log; public class MathTest extends AndroidTestCase { protected int i1; protected int i2; static final String LOG_TAG = "MathTest"; public void setUp() { i1 = 2; i2 = 3; } public void testAdd() { Log.d( LOG_TAG, "testAdd" ); assertTrue( LOG_TAG+"1", ( ( i1 + i2 ) == 5 ) ); } public void testAndroidTestCaseSetupProperly() { super.testAndroidTestCaseSetupProperly(); Log.d( LOG_TAG, "testAndroidTestCaseSetupProperly" ); } }
package aexp.junit; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.provider.Contacts; import android.util.Log; import android.test.AndroidTestCase; public class ContactTest extends AndroidTestCase { static final String LOG_TAG = "ContactTest"; static final String TESTUSER_NAME = "Test User"; static final String TESTUSER_NOTES = "Test note"; ContentResolver contentResolver; Uri newPerson; public void setUp() { contentResolver = getContext().getContentResolver(); ContentValues person = new ContentValues(); person.put(Contacts.People.NAME, TESTUSER_NAME ); person.put(Contacts.People.NOTES, TESTUSER_NOTES ); newPerson = contentResolver.insert( Contacts.People.CONTENT_URI,person); } public void testInsertContact() { Log.d( LOG_TAG, "testInsertContact" ); assertNotNull( newPerson ); } public void testQueryContact() { Log.d( LOG_TAG, "testQueryContact" ); String columns[] = { Contacts.People.NAME, Contacts.People.NOTES }; Cursor c = contentResolver.query( Contacts.People.CONTENT_URI, columns, Contacts.People.NAME+"=?", new String[] { TESTUSER_NAME }, null ); assertNotNull( c ); int hits = 0; while( c.moveToNext() ) { int nameColumnIndex = c.getColumnIndex( Contacts.People.NAME ); int notesColumnIndex = c.getColumnIndex( Contacts.People.NOTES ); String name = c.getString( nameColumnIndex ); String notes = c.getString( notesColumnIndex ); Log.d( LOG_TAG,"retrieved name: "+name ); Log.d( LOG_TAG,"retrieved notes: "+notes ); assertEquals( TESTUSER_NAME, name ); assertEquals( TESTUSER_NOTES, notes ); ++hits; } assertEquals( hits,1 ); c.close(); } public void tearDown() { contentResolver.delete( newPerson, null, null ); } }
子树:
package aexp.junit; import junit.framework.TestSuite; import junit.framework.Assert; public class SomeTest extends TestSuite { public void testSomething() throws Throwable { Assert.assertTrue(1 + 1 == 2); } public void testSomethingElse() throws Throwable { Assert.assertTrue(1 + 1 == 3); } }
测试树:
package aexp.junit; import junit.framework.TestSuite; public class ExampleSuite extends TestSuite { public ExampleSuite() { addTestSuite( MathTest.class ); addTestSuite( ContactTest.class ); addTestSuite(SomeTest.class); } }
测试运行器:
package aexp.junit; import android.app.Activity; import android.os.Bundle; import android.test.AndroidTestCase; import android.test.AndroidTestRunner; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import android.view.View; import android.util.Log; import junit.framework.TestListener; import junit.framework.Test; import junit.framework.AssertionFailedError; public class JUnit extends Activity { static final String LOG_TAG = "junit"; Thread testRunnerThread = null; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Button launcherButton = (Button)findViewById( R.id.launch_button ); launcherButton.setOnClickListener( new View.OnClickListener() { public void onClick( View view ) { startTest(); } } ); } private synchronized void startTest() { if( ( testRunnerThread != null ) && !testRunnerThread.isAlive() ) testRunnerThread = null; if( testRunnerThread == null ) { testRunnerThread = new Thread( new TestRunner( this ) ); testRunnerThread.start(); } else Toast.makeText( this, "Test is still running", Toast.LENGTH_SHORT).show(); } } //显示线程 class TestDisplay implements Runnable { public enum displayEvent{START_TEST,END_TEST,ERROR,FAILURE,} displayEvent ev; String testName; int testCounter; int errorCounter; int failureCounter; TextView statusText; TextView testCounterText; TextView errorCounterText; TextView failureCounterText; public TestDisplay( displayEvent ev, String testName, int testCounter, int errorCounter, int failureCounter, TextView statusText, TextView testCounterText, TextView errorCounterText, TextView failureCounterText ) { this.ev = ev; this.testName = testName; this.testCounter = testCounter; this.errorCounter = errorCounter; this.failureCounter = failureCounter; this.statusText = statusText; this.testCounterText = testCounterText; this.errorCounterText = errorCounterText; this.failureCounterText = failureCounterText; } public void run() { StringBuffer status = new StringBuffer(); switch( ev ) { case START_TEST: status.append( "Starting" ); break; case END_TEST: status.append( "Ending" ); break; case ERROR: status.append( "Error: " ); break; case FAILURE: status.append( "Failure: " ); break; } status.append( ": " ); status.append( testName ); statusText.setText( new String( status ) ); testCounterText.setText( "Tests: "+testCounter ); errorCounterText.setText( "Errors: "+errorCounter ); failureCounterText.setText( "Failure: "+failureCounter ); } } class TestRunner implements Runnable,TestListener { static final String LOG_TAG = "TestRunner"; int testCounter; int errorCounter; int failureCounter; TextView statusText; TextView testCounterText; TextView errorCounterText; TextView failureCounterText; Activity parentActivity; public TestRunner( Activity parentActivity ) { this.parentActivity = parentActivity; } public void run() { testCounter = 0; errorCounter = 0; failureCounter = 0; statusText = (TextView)parentActivity. findViewById( R.id.status ); testCounterText = (TextView)parentActivity. findViewById( R.id.testCounter ); errorCounterText = (TextView)parentActivity. findViewById( R.id.errorCounter ); failureCounterText = (TextView)parentActivity. findViewById( R.id.failureCounter ); Log.d( LOG_TAG, "Test started" ); // 测试运行器 AndroidTestRunner testRunner = new AndroidTestRunner(); testRunner.setTest( new ExampleSuite() ); testRunner.addTestListener( this ); testRunner.setContext( parentActivity ); testRunner.runTest(); Log.d( LOG_TAG, "Test ended" ); } // TestListener public void addError(Test test, Throwable t) { Log.d( LOG_TAG, "addError: "+test.getClass().getName() ); Log.d( LOG_TAG, t.getMessage(), t ); ++errorCounter; TestDisplay td = new TestDisplay( TestDisplay.displayEvent.ERROR, test.getClass().getName(), testCounter, errorCounter, failureCounter, statusText, testCounterText, errorCounterText, failureCounterText ); parentActivity.runOnUiThread( td ); } public void addFailure(Test test, AssertionFailedError t) { Log.d( LOG_TAG, "addFailure: "+test.getClass().getName() ); Log.d( LOG_TAG, t.getMessage(), t ); ++failureCounter; TestDisplay td = new TestDisplay( TestDisplay.displayEvent.FAILURE, test.getClass().getName(), testCounter, errorCounter, failureCounter, statusText, testCounterText, errorCounterText, failureCounterText ); parentActivity.runOnUiThread( td ); } public void endTest(Test test) { Log.d( LOG_TAG, "endTest: "+test.getClass().getName() ); TestDisplay td = new TestDisplay( TestDisplay.displayEvent.END_TEST, test.getClass().getName(), testCounter, errorCounter, failureCounter, statusText, testCounterText, errorCounterText, failureCounterText ); parentActivity.runOnUiThread( td ); } public void startTest(Test test) { Log.d( LOG_TAG, "startTest: "+test.getClass().getName() ); ++testCounter; TestDisplay td = new TestDisplay( TestDisplay.displayEvent.START_TEST, test.getClass().getName(), testCounter, errorCounter, failureCounter, statusText, testCounterText, errorCounterText, failureCounterText ); parentActivity.runOnUiThread( td ); } }
运行结果:
instrumentation框架:
首先新建一个android工程:
里面编写3个activity:MainActivity,HomeActivity,LoginActivity
MainActivity是加载界面,LoginActivity是登陆界面,HomeActivity是最终界面
package cn.edu.wtu.junit; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; public class MainActivity extends Activity { private static final boolean DEBUG = true; private static final String TAG = "-- MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { if (DEBUG) { Log.i(TAG, "onCreate"); } super.onCreate(savedInstanceState); setContentView(R.layout.act_main); View toLoginView = findViewById(R.id.to_login); toLoginView.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { if (DEBUG) { Log.i(TAG, "toLoginView clicked"); } Intent intent = new Intent(getApplicationContext(), LoginActivity.class); startActivity(intent); } }); } }
package cn.edu.wtu.junit; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.EditText; public class LoginActivity extends Activity { private static final boolean DEBUG = true; private static final String TAG = "-- LoginActivity"; private EditText mUsernameView; private EditText mPasswordView; @Override protected void onCreate(Bundle savedInstanceState) { if (DEBUG) { Log.i(TAG, "onCreate"); } super.onCreate(savedInstanceState); setContentView(R.layout.act_login); mUsernameView = (EditText) findViewById(R.id.username); mPasswordView = (EditText) findViewById(R.id.password); View submitView = findViewById(R.id.submit); submitView.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { if (DEBUG) { Log.i(TAG, "submitView clicked"); } Intent intent = new Intent(getApplicationContext(), HomeActivity.class); intent.putExtra(HomeActivity.EXTRA_USERNAME, mUsernameView.getText().toString()); intent.putExtra(HomeActivity.EXTRA_PASSWORD, mPasswordView.getText().toString()); startActivity(intent); } }); View resetView = findViewById(R.id.reset); resetView.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { if (DEBUG) { Log.i(TAG, "resetView clicked"); } mUsernameView.setText(""); mPasswordView.setText(""); mUsernameView.requestFocus(); } }); } }
package cn.edu.wtu.junit; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class HomeActivity extends Activity { private static final boolean DEBUG = true; private static final String TAG = "-- HomeActivity"; public static final String EXTRA_USERNAME = "yuan.activity.username"; public static final String EXTRA_PASSWORD = "yuan.activity.password"; @Override protected void onCreate(Bundle savedInstanceState) { if (DEBUG) { Log.i(TAG, "onCreate"); } super.onCreate(savedInstanceState); Intent intent = getIntent(); StringBuilder sb = new StringBuilder(); sb.append("username:").append(intent.getStringExtra(EXTRA_USERNAME)).append("\n"); sb.append("password:").append(intent.getStringExtra(EXTRA_PASSWORD)); setContentView(R.layout.act_home); TextView loginContentView = (TextView) findViewById(R.id.login_content); loginContentView.setText(sb.toString()); } }
然后新建一个测试工程,基于上面一个project:
package cn.edu.wtu.test; import android.app.Instrumentation; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.test.suitebuilder.annotation.Suppress; import android.util.Log; import android.view.View; import cn.edu.wtu.junit.MainActivity; public class MainActivityTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "=== MainActivityTest"; private Instrumentation mInstrument; private MainActivity mActivity; private View mToLoginView; public MainActivityTest() { super("cn.edu.wtu.junit", MainActivity.class); } @Override public void setUp() throws Exception { super.setUp(); mInstrument = getInstrumentation(); // 启动被测试的Activity mActivity = getActivity(); mToLoginView = mActivity.findViewById(cn.edu.wtu.junit.R.id.to_login); } public void testPreConditions() { // 在执行测试之前,确保程序的重要对象已被初始化 assertTrue(mToLoginView != null); } // @UiThreadTest // 这将会在UI线程里运行方法里所有的语句。不与UI交互的方法不允许这么做 // 注意:waitForIdleSync和sendKeys不允许在UI线程里运行 public void testToLogin() { // @UiThreadTest注解使整个方法在UI线程上执行,等同于上面注解掉的代码 // exception mInstrument.runOnMainSync(new Runnable() { public void run() { mToLoginView.requestFocus(); mToLoginView.performClick(); } }); // mActivity.runOnUiThread(new Runnable(){ // // @Override // public void run() { // // mToLoginView.requestFocus(); // mToLoginView.performClick(); // } // // }); } @Suppress public void testNotCalled() { // 使用了@Suppress注解的方法不会被测试 Log.i(TAG, "method 'testNotCalled' is called"); } @Override public void tearDown() throws Exception { super.tearDown(); } }
package cn.edu.wtu.test; import android.app.Instrumentation; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.EditText; import cn.edu.wtu.junit.LoginActivity; public class LoginActivityTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "=== LoginActivityTest"; private Instrumentation mInstrument; private LoginActivity mActivity; private EditText mUsernameView; private EditText mPasswordView; private View mSubmitView; private View mResetView; public LoginActivityTest() { super("cn.edu.wtu.junit", LoginActivity.class); } @Override public void setUp() throws Exception { super.setUp(); /* * 关闭触屏模式为了控制从测试程序中发送给模拟器或设备的按键事件,你必须关闭触屏模式。如果你不这么做,按键事件将被忽略。 * 你需要在调用getActivity()启动Activity之前调用ActivityInstrumentationTestCase2.setActivityTouchMode(false)。 * 你必须在非UI线程中运行这个调用。基于这个原因,你不能在声明有@UIThread的测试方法调用。可以在setUp()中调用。 * 要向程序发送key事件的话,必须在getActivity之前调用该方法来关闭touch模式 * 否则key事件会被忽略 */ setActivityInitialTouchMode(false); mInstrument = getInstrumentation(); mActivity = getActivity(); Log.i(TAG, "current activity: " + mActivity.getClass().getName()); mUsernameView = (EditText) mActivity.findViewById(cn.edu.wtu.junit.R.id.username); mPasswordView = (EditText) mActivity.findViewById(cn.edu.wtu.junit.R.id.password); mSubmitView = mActivity.findViewById(cn.edu.wtu.junit.R.id.submit); mResetView = mActivity.findViewById(cn.edu.wtu.junit.R.id.reset); } public void testPreConditions() { assertTrue(mUsernameView != null); assertTrue(mPasswordView != null); assertTrue(mSubmitView != null); assertTrue(mResetView != null); } public void testInput() { input(); assertEquals("yuan", mUsernameView.getText().toString()); assertEquals("1123", mPasswordView.getText().toString()); } public void testSubmit() { input(); mInstrument.runOnMainSync(new Runnable() { public void run() { mSubmitView.requestFocus(); mSubmitView.performClick(); } }); } public void testReset() { input(); mInstrument.runOnMainSync(new Runnable() { public void run() { mResetView.requestFocus(); mResetView.performClick(); } }); assertEquals("", mUsernameView.getText().toString()); assertEquals("", mPasswordView.getText().toString()); } @Override public void tearDown() throws Exception { super.tearDown(); } private void input() { mActivity.runOnUiThread(new Runnable() { public void run() { mUsernameView.requestFocus(); } }); // 因为测试用例运行在单独的线程上,这里最好要 // 同步application,等待其执行完后再运行 mInstrument.waitForIdleSync(); sendKeys(KeyEvent.KEYCODE_Y, KeyEvent.KEYCODE_U, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_N); // 效果同上面sendKeys之前的代码 mInstrument.runOnMainSync(new Runnable() { public void run() { mPasswordView.requestFocus(); } }); sendKeys(KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3); } }
package cn.edu.wtu.test; import android.content.Intent; import android.test.ActivityUnitTestCase; import android.widget.TextView; import cn.edu.wtu.junit.HomeActivity; public class HomeActivityTest extends ActivityUnitTestCase { private static final String TAG = "=== HomeActivityTest"; private static final String LOGIN_CONTENT = "username:yuan\npassword:1123"; private HomeActivity mHomeActivity; private TextView mLoginContentView; public HomeActivityTest() { super(HomeActivity.class); } @Override public void setUp() throws Exception { super.setUp(); Intent intent = new Intent(); intent.putExtra(HomeActivity.EXTRA_USERNAME, "yuan"); intent.putExtra(HomeActivity.EXTRA_PASSWORD, "1123"); // HomeActivity有extra参数,所以我们需要以intent来启动它 mHomeActivity = launchActivityWithIntent("cn.edu.wtu.junit", HomeActivity.class, intent); mLoginContentView = (TextView) mHomeActivity.findViewById(cn.edu.wtu.junit.R.id.login_content); } public void testLoginContent() { assertEquals(LOGIN_CONTENT, mLoginContentView.getText().toString()); } @Override public void tearDown() throws Exception { super.tearDown(); } }
run on android unit:
参考:
http://www.ibm.com/developerworks/cn/java/j-lo-junit-src/
http://www.moandroid.com/?page_id=1176
http://www.ibm.com/developerworks/cn/java/j-lo-junit4/
http://www.cnblogs.com/xirihanlin/archive/2010/06/15/1758677.html
转:http://www.open-open.com/lib/view/open1328152424546.html