现在的 App 一般都是 Hybrid 的,常见的情况是有一个 Native 的按钮,点了之后会打开一个 Webview 界面,那么在 Espresso 里如何测试呢?现在我们从 0 开始,来分析此类 Webview 该如何测试.

正所谓不入虎穴焉得虎子!一个好的测试人员应该知其然,更知其所以然!不要害怕,难道写一个简单的 App 这么难吗?在你下定决心的时候,就已经赢了一半! 你不开始,就永远没有深入了解它是如何运作的机会!Never!

  1. 新建项目

    打开 Android Studio, File>New Project, Application name 随便取,然后 Next>Next,选择默认的 Empty Activity, Next>Finish. 除了命名外,我们全部采用默认的配置,建了一个空的 Activity。什么是 Activity 呢? 你可以简单的理解为一个页面,对,就是一个页面,就是你最常见到的页面,仅此而已,所有页面上的变化,不过是修改了部分显示控件的内容而已。就这么简单!我们建了一个 Activity,就可以在这个页面上随便绘制想要的内容:按钮啦,输入框啦等等等等……建完 Project 之后,AS(Android Studio) 会开始 build 你的项目 (没错,AS 就是这么严谨,哪怕是空的项目啥也没有,它也会默认帮你编译一遍,看是否缺什么依赖!Google 棒棒哒!),不出意外,build 完了之后大概是下面的样子 (以下是 Mac 系统下的目录,Windows 的请参照这个):

    大概每个目录的意思:

    1. .gradle: Gradle 编译系统,版本由下面的gradle>wrapper指定
    2. .idea:IDE 生成的专用工程配置文件,类似 Eclipse 的.project, VSCode 的.vscode一样
    3. app:项目的所有代码和资源文件

      1. app/build:app 模块编译输出的文件
      2. app/libs: 放置引用的类库文件
      3. app/src: 放置应用的主要文件目录
      4. app/src/androidTest:单元测试目录
      5. app/src/main:主要的项目目录和代码
      6. app/src/main/assets:放置原生文件,里面的文件会保留原有格式,文件的读取需要通过流
      7. app/src/main/java:项目的源代码
      8. app/src/main/res:项目的资源
      9. app/src/main/res/drawable:存放各种位图文件 (.png,.jpg,.9png,.gif 等) 和 drawable 类型的 XML 文件
      10. app/src/main/res/drawable-v24:存放自定义 Drawables 类(Android API 24 开始,可在 XML 中使用)
      11. app/src/main/res/layout:存放布局文件
      12. app/src/main/res/menu:存放菜单文件
      13. app/src/main/res/mipmap-hdpi:存放高分辨率图片资源
      14. app/src/main/res/mipmap-mdpi:存放中等分辨率图片资源
      15. app/src/main/res/mipmap-xdpi:存放超高分辨率图片资源
      16. app/src/main/res/mipmap-xxdpi:存放超超分辨率图片资源
      17. app/src/main/res/mipmap-xxxdpi:存放超超超高分辨率图片资源
      18. app/src/main/res/raw:存放各种原生资源 (音频,视频,一些 XML 文件等)
      19. app/src/main/res/values: 存放各种配置资源(颜色,尺寸,样式,字符串等)
      20. app/src/main/res/values/attrs.xml:自定义控件时用的较多,自定义控件的属性
      21. app/src/main/res/values/arrays.xml:定义数组资源
      22. app/src/main/res/values/colors.xml:定义颜色资源
      23. app/src/main/res/values/dimens.xml:定义尺寸资源
      24. app/src/main/res/values/string.xml:定义字符串资源
      25. app/src/main/res/values/styles.xml:定义样式资源
      26. app/src/main/res/values-v11:在 API 11+ 的设备上调用
      27. app/src/main/res/values-v14:在 API 14+ 的设备上调用
      28. app/src/main/res/values-v21:在 API 21+ 的设备上调用
      29. app/src/main/res/AndroidManifest.xml:项目的清单文件(名称、版本、SDK、权限等配置信息)
      30. app/src/.gitignore:忽略的文件或者目录
      31. app/app.iml:app 模块的配置文件
      32. app/build.gradle:app 模块的 gradle 编译文件
      33. app.*:app 模块的代码混淆配置文件
    4. build:系统生成的文件目录

    5. gradle: wrapper 的 jar 和配置文件所在的位置

    6. .gitignore: 忽略的文件或者目录

    7. build.gradle:项目的 gradle 编译文件

    8. gradle.properties: gradle 相关的全局属性设置

    9. gradlew: 编译脚本,可以在命令行执行打包

    10. gradlew.bat:windows 下的 gradle wrapper 可执行文件

    11. local.properties:配置 SDK/NDK 所在的路径

    12. settings.gradle:设置相关的 gradle 脚本

    13. webViewDemo.iml:项目模块的相关信息

    14. External Libraries:项目依赖的库,编译时自动下载

      这些信息仅供参考,我们大概了解下就行,当然,以后深入了,我们会对以上目录或文件会有更深的理解!

  2. 编写源码

app 的源码在app/src/main/java目录下,复制如下代码替换 MainActivity 里的内容 (注意替换为自己的包名,需要根据自己的情况进行替换):

MainActivity.java:

package com.example.pis.webviewdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;


public class MainActivity extends AppCompatActivity {

    public static final String TAG_WEB_VIEW_EXAMPLE = "WEB_VIEW_EXAMPLE";

    private EditText urlEditor;

    private Button loadUrlButton;

    private Button backButton;

    private Button forwardButton;

    private Button clearCacheButton;

    private Button showSnippetButton;

    private WebView webView;

    private CustomWebviewClient customWebviewClient;

    private CustomWebChromeClient customWebChromeClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Request show progress bar in the activity window title.
        Window activityWindow = this.getWindow();
        activityWindow.requestFeature(Window.FEATURE_PROGRESS);

        setContentView(R.layout.activity_main);

        setTitle("dev2qa.com - Android WebView Example.");

        // Initialize this activity used controls.
        initControl();

        // Click this button to load user specified web url page.
        loadUrlButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String url = urlEditor.getText().toString();
                if(!TextUtils.isEmpty(url))
                {
                    webView.loadUrl(url);
                }else {
                    webView.loadData("<font color=red><b>Please input web page url start with http or https, then click load button.</b></font>", "text/html", "utf-8");
                }
            }
        });

        // Click this button to go to previous page.
        backButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(webView.canGoBack())
                {
                    webView.goBack();
                }
            }
        });

        // Click this button to go to next page.
        forwardButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(webView.canGoForward())
                {
                    webView.goForward();
                }
            }
        });

        // Click this button to clear webview cache, history and html form data.
        clearCacheButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Clear cache page.
                webView.clearCache(true);

                // Clear webview history.
                webView.clearHistory();

                // Clear html form data.
                webView.clearFormData();
            }
        });

        showSnippetButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                webView.loadData("<strong><font color=red>Hello WebView</font></strong>", "text/html", "utf-8");
            }
        });
    }

    // Initialize the edit text, buttons and WebView component.
    private void initControl()
    {

        urlEditor = (EditText)findViewById(R.id.web_view_url_editor);

        loadUrlButton = (Button)findViewById(R.id.web_view_load_button);

        backButton = (Button)findViewById(R.id.web_view_back_button);

        forwardButton = (Button)findViewById(R.id.web_view_forward_button);

        clearCacheButton = (Button)findViewById(R.id.web_view_clear_cache_button);

        showSnippetButton = (Button)findViewById(R.id.web_view_show_snippet_html);

        webView = (WebView)findViewById(R.id.web_view_component);

        WebSettings webSettings = webView.getSettings();

        webSettings.setJavaScriptEnabled(true);

        // Set custom webview client.
        customWebviewClient = new CustomWebviewClient();
        webView.setWebViewClient(customWebviewClient);

        // Set custom web chrome client.
        customWebChromeClient = new CustomWebChromeClient();
        customWebChromeClient.setSourceActivity(this);
        webView.setWebChromeClient(customWebChromeClient);

        webView.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
    }


    @Override
    protected void onDestroy() {
        // Destroy WebView object when activity is destroyed.
        if(webView!=null)
        {
            ViewGroup viewGroup = (ViewGroup) webView.getParent();
            viewGroup.removeView(webView);

            webView.destroy();
            webView = null;
        }

        super.onDestroy();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if(keyCode == KeyEvent.KEYCODE_BACK)
        {
            // Process android device back menu
            if(webView!=null && webView.canGoBack())
            {
                webView.goBack();
                return true;
            }
        }
        return super.onKeyDown(keyCode, event);
    }
}

CustomWebviewClient.java:

package com.example.pis.webviewdemo;

/**
 * Created by pis on 2019/2/19.
 */

import android.graphics.Bitmap;
import android.util.Log;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;

/**
 * Created by Jerry on 2/26/2018.
 */

public class CustomWebviewClient extends WebViewClient {

    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
        super.onReceivedError(view, request, error);
    }

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onPageStarted url : " + url);
        super.onPageStarted(view, url, favicon);
    }

    @Override
    public void onPageFinished(WebView view, String url) {
        Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onPageFinished url : " + url);
        super.onPageFinished(view, url);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        if(request!=null) {
            Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "shouldOverrideUrlLoading request.toString() : " + request.toString());
            Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "shouldOverrideUrlLoading request.getUrl().toString() : " + request.getUrl().toString());
            view.loadUrl(request.getUrl().toString());
        }else
        {
            Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "shouldOverrideUrlLoading request is null.");
        }
        return false;
    }
}

CustomWebChromeClient:

package com.example.pis.webviewdemo;

import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
/**
 * Created by pis on 2019/2/19.
 */

public class CustomWebChromeClient extends WebChromeClient {

    private AppCompatActivity sourceActivity;

    private String webPageTitle = "";

    public AppCompatActivity getSourceActivity() {
        return sourceActivity;
    }

    public void setSourceActivity(AppCompatActivity sourceActivity) {
        this.sourceActivity = sourceActivity;
    }

    // This method is invoked when webview load page.
    @Override
    public void onProgressChanged(WebView view, int newProgress) {

        // set title and
        sourceActivity.setTitle("Page Loading...... - dev2qa.com");
        int showProgress = newProgress * 100;
        sourceActivity.setProgress(showProgress);

        if(newProgress == 100)
        {
            sourceActivity.setTitle(webPageTitle);
        }

        super.onProgressChanged(view, newProgress);
        Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onProgressChanged newProgress : " + newProgress);
    }

    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
        webPageTitle = title;
        Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onReceivedTitle title : " + title);
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onJsAlert url : " + url + " , message : " + message);
        return super.onJsAlert(view, url, message, result);
    }

    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onJsConfirm url : " + url + " , message : " + message);
        return super.onJsConfirm(view, url, message, result);
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onJsPrompt url : " + url + " , message : " + message);
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
}

以上三个文件都是放在app/src/main/java目录下。

下面,我们还需要简单的布局文件,在 src>main>res 下面,有一个 layout 目录,目录下面默认有一个 activity_main.xml 文件,复制以下内容替换自己的文件内容:

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="com.example.pis.webviewdemo.MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <EditText
            android:id="@+id/web_view_url_editor"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Input a url to browse."/>

        <LinearLayout
            android:id="@+id/web_view_button_layout"
            android:layout_below="@id/web_view_url_editor"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <Button
                android:id="@+id/web_view_load_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Load"/>

            <Button
                android:id="@+id/web_view_back_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Prev"/>

            <Button
                android:id="@+id/web_view_forward_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Next"/>

            <Button
                android:id="@+id/web_view_clear_cache_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Clear"/>

            <Button
                android:id="@+id/web_view_show_snippet_html"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Hello" />

        </LinearLayout>

        <WebView
            android:id="@+id/web_view_component"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/web_view_button_layout"
            android:background="@color/colorPrimaryDark"></WebView>

    </RelativeLayout>

</android.support.constraint.ConstraintLayout>

注意上面的tools:context行,声明了当前这个布局文件是给哪个 Activity 服务的,你需要替换为自己的对应的 Activity 名,其他的保持不变.
另外,我们的 src>main 下面的AndroidMainfest.xml如下供参考:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.pis.webviewdemo">

    <!-- Play web url audio file required permission. -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name="com.example.pis.webviewdemo.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

现在,我们开始试着编译了!一般情况下,新手会遇到一些问题,都是一些简单的问题,相信你会很快修改完毕,并编译成功 (比如你可能会遇到最低版本要求问题:代码里有个函数 getUrl() 是在 sdk>21 才能使用的,因此你可能需要改一下 minSdkVersion 为 21,在哪儿改呢?相信你一定会找到~),有问题可以通过搜索引擎来搜索一下,以上问题都是基本的问题,很容易调试完成!调试成功运行的界面如下图:

输入一个 url,比如http://www.bing.com就可以看到网址被 load 成功了,说明我们的 webview 起作用了!


↙↙↙阅读原文可查看相关链接,并与作者交流