Android+uiautomator2.0实现离线运行UI测试
- 目的
- 参考文档
- 实现
- 工程搭建
- uiautomator用例实现
- Android创建APK流程以及出现的问题
目的
新建Android工程,实现安装apk到手机后,点击UI界面的按钮执行基于uiautomator实现的UI自动化用例。
参考文档
1、https://blog.csdn.net/cxq234843654/article/details/52605441
2、https://blog.csdn.net/pgz100/article/details/82971699
3、https://blog.csdn.net/cxq234843654/article/details/51557025
4、https://zhuanlan.zhihu.com/p/148448688
本文主要在以上的教程上实验补充
实现
工程搭建
1、新建一个Android应用
选择Empty Activity,一路next到finish
2、给UI增加Button,修改res/layout目录下activity_main.xml文件
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:onClick="runMyUiautomator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="53dp"
android:layout_marginTop="117dp"
android:text="去相机拍照"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
给button添加click事件,当点击button事,去MainActivity.java执行runMyUiautomator方法
3、修改MainActivity.java文件MainActivity Class,添加Button事件、uiautomator线程、创建CMDUtils类等
package com.example.secondapp;
import androidx.appcompat.app.AppCompatActivity;
import andorid.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import anroid.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button= findViewById(R.id.button);
}
/**
* 点击按钮对应的方法
* @param v
*/
public void runMyUiautomator(View v){
Log.i(TAG, "runMyUiautomator: ");
new cameraTest().start();
Toast.makeText(this, "start run", Toast.LENGTH_SHORT).show();
}
/**
* 运行uiautomator是个费时的操作,不应该放在主线程,因此另起一个线程运行
*/
class cameraTest extends Thread {
@Override
public void run() {
super.run();
String command = generateCommand("com.example.mytestcase", "ExampleInstrumentedTest ", "BackLens_snapshotTest");
ShellUtils.CMD_Result rs= ShellUtils.runCMD(command,true,true);
Log.e(TAG, "run: " + rs.error + "-------" + rs.success);
}
/**
* 生成命令
* @param pkgName uiautomator包名
* @param clsName uiautomator类名
* @param mtdName uiautomator方法名
* @return
*/
public String generateCommand(String pkgName, String clsName, String mtdName) {
String command = "am instrument -w -r -e debug false -e class "
+ pkgName + "." + clsName + "#" + mtdName + " "
+ pkgName + ".test/androidx.support.test.runner.AndroidJUnitRunner";
Log.e("test1: ", command);
return command;
}
}
}
package com.example.secondapp;
import android.util.Log;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.List;
/**
* 执行命令
*/
public class ShellUtils {
private static final String TAG = "CMDUtils";
public static class CMD_Result {
public int resultCode;
public String error;
public String success;
public CMD_Result(int resultCode, String error, String success) {
this.resultCode = resultCode;
this.error = error;
this.success = success;
}
}
/**
* 执行命令
*
* @param command 命令
* @param isShowCommand 是否显示执行的命令
* @param isNeedResultMsg 是否反馈执行的结果
* @retrun CMD_Result
*/
public static CMD_Result runCMD(String command, boolean isShowCommand,
boolean isNeedResultMsg) {
if (isShowCommand)
Log.i(TAG, "runCMD:" + command);
CMD_Result cmdRsult = null;
int result;
Process process = null;
PrintWriter pw = null;
try {
process = Runtime.getRuntime().exec("sh"); //获取root权限
pw = new PrintWriter(process.getOutputStream());
pw.println(command);
pw.flush();
result = process.waitFor();
if (isNeedResultMsg) {
StringBuilder successMsg = new StringBuilder();
StringBuilder errorMsg = new StringBuilder();
BufferedReader successResult = new BufferedReader(
new InputStreamReader(process.getInputStream()));
BufferedReader errorResult = new BufferedReader(
new InputStreamReader(process.getErrorStream()));
String s;
while ((s = successResult.readLine()) != null) {
successMsg.append(s);
}
while ((s = errorResult.readLine()) != null) {
errorMsg.append(s);
}
cmdRsult = new CMD_Result(result, errorMsg.toString(),
successMsg.toString());
}
} catch (Exception e) {
Log.e(TAG, "run CMD:" + command + " failed");
e.printStackTrace();
} finally {
if (pw != null) {
pw.close();
}
if (process != null) {
process.destroy();
}
}
return cmdRsult;
}
}
4、新建一个Module,用于编写uiautomator脚本
单击项目名称,右击“New – Module – Phone&Table Module"
这里我新建了mytestcase
5、修改mytestcase模块的build.gradle文件
打开build.gradle文件,可以看到系统已经自动在defaultConfig中添加了Runner(testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner”)
build.gradle(:mytestcase)
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.example.mytestcase"
minSdkVersion 29
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJunitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir:"libs",include:["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
# UiAutomator Testing Need
androidTestImplementation 'com.android.support:support-annotations:23.1.1'
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.2.1'
androidTestImplementation 'com.android.support.test:0.4.1'
}
6、编写uiautomator脚本并运行
打开系统自行创建的测试类ExampleInstrumentedTest.java,
原理见https://blog.csdn.net/pgz100/article/details/82971699
具体测试用例见第8章
7、给APP添加系统签名
7.1、app目录下的AndroidManifest.xml中添加android:sharedUserId=“android.uid.system”
7.2、在build->Generate signed bundle/apk生成jks
7.3、使用keytool-importkeypair对jks文件引入系统签名
1、下载keytool工具,下载链接:https://github.com/getfatday/keytool-importkeypair
2、下载对应的platform.x509.pem、platform.pk8,文件在源码位置: android/build/target/product/security/,下载链接(这个是P版本对应的链接):https://android.googlesource.com/platform/build/+/pie-release/target/product/security
3、把platform.x509.pem、platform.pk8和上一部生成的jks文件统一放到一个linux目录文件夹下,执行以下的步骤:
./keytool-importkeypair -k [jks文件名] -p [jks的密码] -pk8 platform.pk8 -cert platform.x509.pem -alias [jks的别名]
如:
./keytool-importkeypair -k secondapp.jks -p 123456 -pk8 platform.pk8 -cert platform.x509.pem -alias test
这个文件名密码等是在7.2节生成的。
然后将新生成的jks导出来,比如我保存在本地电脑的d盘路径,如:D:\exercises\secondApp\new\secondapp.jks
4、如果遇到./keytool-importkeypair:Permission denied,则:
chmod 777 keytool-importkeypair
再重复步骤3
5、配置gradle(app)
在build.gradle(:app)中更新signingConfigs
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.example.secondapp"
minSdkVersion 29
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJunitRunner"
}
signingConfigs {
debug {
storeFile file("D:\\exercises\\secondApp\\new\\secondapp.jks")
storePassword '123456'
keyAlias 'test'
keyPassword '123456'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir:"libs",include:["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
# UiAutomator Testing Need
androidTestImplementation 'com.android.support:support-annotations:23.1.1'
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.2.1'
androidTestImplementation 'com.android.support.test:0.4.1'
}
8、通过MyTest APP启动uiautomator脚本,实现脱离PC运行
uiautomator用例实现
以测试huawei自带camera apk为例
1、知识点
1、使用dumpWindowHierarchy方法,得到ui界面上的控件信息,如id等,然后进行点击等后续操作
2、使用performTwoPointerGesture进行ZOOM等操作
3、通过dropbox检测异常信息
2、代码实现
2.1、基础架构
package com.example.mytestcase;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import android.content.ContextWrapper;
import android.graphics.Point;
import android.os.Environmnet;
import android.os.SystemClock;
import anroid.util.Log;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.*;
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest{
@Test
public void useAppContext(){
Context appContext = InstrmentationRegistry.getInstrmentation().getTargetContext();
assertEquals("com.example.secondapp",appContext.getPackageName());
}
private static final String PACKAGE_TAG ="ApplicationTest";
private static final int MAX_STRESS_TEST_COUNT = 10;
private UiDevice mDevice;
@BeforeClass
public static void InitialUiDevice(){
LOG.i(PACKAGE_TAG,"initialize uidevice beforeclass");
}
@Before
public void PrepareWorkBeforeTest() throws UiObjectNotFoundException{
LOG.i(PACKAGE_TAG,"PrepareWorkBeforeTest");
mDevice = UiDevice.getInstance(InstrmentationRegistry.getInstrmentation());
assertThat(mDevice,notNullValue());
mDevice.setCompressedLayoutHeirarchy(true);
mDevice.pressBack();
mDevice.pressHome();
}
@Test
public void BackSnapshotTest() throws UiObjectNotFoundException{
LOG.i(PACKAGE_TAG,"BackSnapshotTest");
mDevice.findObject(new UiSelector().text("Camera")).clickAndWaitForNewWindow();
UiObject captureObj = mDevice.findObject(new UiSelector().resourceId("com.huawei.camera:id/shutter_button");
for(int i=0;i<MAX_STRESS_TEST_COUNT;i++){
captureObj.click();
SystemClock.sleep(10);
}
}
@After
public void ClearWorkAfterCameraTest(){
LOG.i(PACKAGE_TAG,"ClearWorkAfterCameraTest");
if(mDevice!=null){
mDevice = null;
}
}
@AfterClass
public static void EndWork(){
LOG.i(PACKAGE_TAG,"EndWork");
}
}
2.2、dumpWindowHierarchy
如果不知道ui控件的id信息等,可是用 dumpWindowHierarchy方法得到当前界面的ui信息到指定路径,然后从指定路径读取文件获取指定的控件进行后续操作
private void dumpXml(){
File file = new File("指定路径");
try{
if(!file.exist()){
boolean flag = file.createNewFile();
}
mDevice.dumpWindowHierarchy(file);
}catch(IOException e){
e.printStackTrace();
}
}
private void parseXml(){
BufferedReader in;
String str = "";
try{
in = new BufferedReader(new FileReader("指定路径");
String line;
while((line=in.readLine())!=null){
if(line.contains("xxxx"){
str+=line;
}
}
}catch(IOException e){
e.printStackTrace();
}
}
Android创建APK流程以及出现的问题
-
出现failed to find platform sdk with path ?
去setting->system setting->android sdk中的android sdk下载对应的sdk。 -
android studio打包build中没有generate signed apk?
点击file->sync project with gradle files,然后重启android studio,generate signed apk就出现了。 -
在dependencies中添加依赖,然后点击sync project with gradle files,使用project打开项目,在externed library中就可以看到引入的lib包