使用:
1、添加依赖
androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3'
2、添加测试代码,例如:
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> activityTestRule =
new ActivityTestRule<>(MainActivity.class);
private Solo solo;
@Before
public void setUp() throws Exception{
// 获取activity
solo = new Solo(InstrumentationRegistry.getInstrumentation(),activityTestRule.getActivity());
}
@After
public void tearDown() throws Exception {
solo.finishOpenedActivities(); // 关闭打开的活动
}
@Test
public void testAddNote() throws Exception {
solo.clickOnView(solo.getView(R.id.test)); // 点击view
}
}
robotium框架是通过模拟用户操作手机屏幕来进行测试,几个基本操作:
- 要知道操作控件的坐标
- 对控件进行模拟操作
- 判断操作的结果是否符合预期
控件的获取
1、通过id获取
solo.getView(R.id.test);
2、通过索引、文本获取
solo.getText(0); // 第一个Textview控件
solo.getText("hello"); // 包含"hello"文字的textView控件(文字部分匹配即可)
3、通过对控件类型进行过滤获取
// 比如:一个LinearLayout中子控件都是动态添加的,就可以用这种方式获取
LinearLayout parentView = (LinearLayout) solo.getView(R.id.test);
ArrayList<TextView> textViews = solo.getCurrentViews(TextView.class, parentView);
textViews.get(0);
// getCurrentViews()方法有4个重载方法,可以指定过滤条件
控件的操作
1、点击、长按操作
clickOnView(View view) / clickLongOnView(View view)
clickOnScreen(float x,float y) / clickLongOnScreen(float x,float y)
2、输入框操作
enterText(EditText editText,String text) // 直接对文本框赋值
typeText(EditText editText,String text) // 一个一个文本的输入
clearText(EditText editText)
3、滑动、滚动
// 从起始坐标滑动到终点坐标,通过setCount参数指定滑动的步长
drag(float fromX, float toX, float fromY, float toY, int setpCount)
// 滑动至屏幕的顶部/滑动至屏幕的顶部底部
scrollToTop()/scrollToDown()
// 向上滑一屏/向下滑一屏(不能触发ListView的加载更多的监听)
scrollUp()/scrollDown()
4、搜索和等待
sleep(int time) // 休眠
boolean searchText(String text) // 从当前页面搜索指定文本
boolean waitForView(int id)/waitForText(String text) // 等待指定控件/文本出现
boolean waitForActivity(String name) // 等待指定的Activity出现
boolean waitForLogMessage(String logMessage) // 等待指定的日志信息出现
boolean waitForDialogToOpen()/waitForDialogToClose() // 等待指定弹窗打开/关闭
Robotium中查找控件、点击控件等api都默认使用了搜索等待机制,因此非特殊情况是不用添加额外的等待操作。
5、截图及其他
takeScreenshot(String name) // 截图,图片名为name,默认路径为/sdcard/Robotium-Screenshots
finishOpenedActivities() // 关闭已经打开的所有activity
goBack() / goBackToActivity(String name) // 点击返回键/不断的点击返回键直至返回到指定的activity
hideSoftKeyboard() // 收起键盘
setActivityOrientation(int orientation) // 设置Activity转屏方向
其他:Robotium 4.0开始支持对webView的进行测试,提供了相关的api,步骤和native测试一样,获取元素、操作、断言。
断言
1、Junit中的断言
// condition应该为true。当condition为false时,抛出Message异常
void assertTrue(String message, boolean condition)
// condition应该为false。当condition为true时,抛出Message异常
void assertFalse(String message, boolean condition)
// 直接使用例失败,并抛出message异常
void fail(String message)
2、Robotium中的断言
// 当前页面是否为name参数指定的activity
void assertCurrentActivity(String message, String name)
// 当前是否处于低内存状态
void assertMemoryNowLow()
3、android中断言的api
// 断言view是否在屏幕中
assertOnSreen(View origin, View view)
// 断言两个View是否底端对齐,即它们的底端y坐标是否相等
assertBottomAligned(View first, View second)
...
源码解析
getView获取流程:
public View getView(int id) {
return this.getView(id, 0);
}
public View getView(int id, int index) {
View viewToReturn = this.getter.getView(id, index); // 这个方法下面会分析
if(viewToReturn == null) { // 没有获取到view,断言失败
String resourceName = "";
try {
resourceName = this.instrumentation.getTargetContext().getResources().getResourceEntryName(id);
} catch (Exception var6) {
Log.d(this.config.commandLoggingTag, "unable to get resource entry name for (" + id + ")");
}
int match = index + 1;
if(match > 1) {
Assert.fail(match + " Views with id: \'" + id + "\', resource name: \'" + resourceName + "\' are not found!");
} else {
Assert.fail("View with id: \'" + id + "\', resource name: \'" + resourceName + "\' is not found!");
}
}
return viewToReturn;
}
下面看this.getter.getView(id, index)方法
public View getView(int id, int index) {
return this.getView(id, index, 0);
}
public View getView(int id, int index, int timeout) {
return this.waiter.waitForView(id, index, timeout);
}
public View waitForView(int id, int index, int timeout) {
if(timeout == 0) {
timeout = Timeout.getSmallTimeout(); // 初始化的时候会设置超时时间为10s
}
return this.waitForView(id, index, timeout, false);
}
public View waitForView(int id, int index, int timeout, boolean scroll) {
HashSet uniqueViewsMatchingId = new HashSet();
long endTime = SystemClock.uptimeMillis() + (long)timeout;
while(SystemClock.uptimeMillis() <= endTime) {
this.sleeper.sleep(); // Thread.sleep(500);
Iterator i$ = this.viewFetcher.getAllViews(false).iterator(); // 通过反射获取到当前页面所有的view
while(i$.hasNext()) {
View view = (View)i$.next();
Integer idOfView = Integer.valueOf(view.getId());
if(idOfView.equals(Integer.valueOf(id))) { // 获取到控件
uniqueViewsMatchingId.add(view);
if(uniqueViewsMatchingId.size() > index) {
return view; // 返回控件
}
}
}
if(scroll) {
this.scroller.scrollDown();
}
}
// 在endTime时间内还没获取到控件,则返回null
return null;
}
总结一下就是,通过反射获取当前页面的view,在10s内不断循环,和当前的getView传入的id配置是否相同,如果相同就返回view。
下面看一下clickOnView
public void clickOnView(View view) {
view = this.waiter.waitForView(view, Timeout.getSmallTimeout());
this.clicker.clickOnScreen(view);
}
先看waitForView()方法
public View waitForView(View view, int timeout) {
return this.waitForView(view, timeout, true, true);
}
public View waitForView(View view, int timeout, boolean scroll, boolean checkIsShown) {
long endTime = SystemClock.uptimeMillis() + (long)timeout;
int retry = 0;
if(view == null) {
return null;
} else {
label39:
do {
for(; SystemClock.uptimeMillis() < endTime; this.sleeper.sleep()) {
boolean foundAnyMatchingView = this.searcher.searchFor(view);
if(checkIsShown && foundAnyMatchingView && !view.isShown()) {
// 如果找到这个view,这个view没有显示,则sleep 300ms
this.sleeper.sleepMini();
++retry;
View identicalView = this.viewFetcher.getIdenticalView(view);
if(identicalView != null && !view.equals(identicalView)) {
view = identicalView;
}
continue label39;
}
if(foundAnyMatchingView) { // 如果找到匹配的view则返回
return view;
}
if(scroll) {
this.scroller.scrollDown();
}
}
return view;
} while(retry <= 5);
return view;
}
}
下面看clickOnScreen()方法
public void clickOnScreen(View view) {
this.clickOnScreen(view, false, 0);
}
public void clickOnScreen(View view, boolean longClick, int time) {
if(view == null) {
Assert.fail("View is null and can therefore not be clicked!");
}
float[] xyToClick = this.getClickCoordinates(view); // 获取点击的坐标
float x = xyToClick[0];
float y = xyToClick[1];
if(x == 0.0F || y == 0.0F) {
this.sleeper.sleepMini();
try {
view = this.viewFetcher.getIdenticalView(view); // 获取找到的view
} catch (Exception var8) {
;
}
if(view != null) {
xyToClick = this.getClickCoordinates(view);
x = xyToClick[0];
y = xyToClick[1];
}
}
if(longClick) {
this.clickLongOnScreen(x, y, time, view);
} else {
this.clickOnScreen(x, y, view); // 点击view
}
}
总结一下就是获取view,然后获取view的坐标,触发点击事件。