[译]使用 Espresso 测试一个有序列表

原文:Testing a sorted list with Espresso
作者:Egor Andreevici
译者:lovexiaov

Espresso 是一个十分强大的工具,可以用它为 Android 编写验收测试。所谓验收测试是指正确实现了所有特性(或某些方面的特性)。自动化验收测试的优势在于简单的捕捉回归,这在积极开发阶段和 bug 修复阶段很常见。在你写完自动化测试之后,查看新的修改是否引入了问题将变得简单。太棒啦!

本文将向你展示如何在你的 Android 工程中设置 Espresso,并会写一个简单的验收测试来检验一组英超联赛团队是否按字母顺序排序。给自己冲杯咖啡(译者注:Espresso 是一种咖啡)并系好你的安全带哟!

Espresso 设置

如果你使用 Android Studio 和 Gradle,那么配置 Espresso 对你来说将非常简单。你只需要打开 app 模块下的 build.gradle 文件,并添加以下依赖:

def APPCOMPAT_VERSION = "23.1.1"  
def ESPRESSO_RUNNER_VERSION = "0.4.1"

dependencies {  
    // dependencies with "compile" scope go here

    androidTestCompile "com.android.support:support-annotations:${APPCOMPAT_VERSION}"
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    androidTestCompile "com.android.support.test:runner:${ESPRESSO_RUNNER_VERSION}"
    androidTestCompile "com.android.support.test:rules:${ESPRESSO_RUNNER_VERSION}"
}

顺便说一下,此工程的完整代码可托管在GitHub上,你可以免费获取。

使用 Gradle 同步你的工程,在 Gradle 构建时你可以抿一口咖啡。完成配置还有最后一步,在 build.gradle 文件的 defaultConfig 语句块中添加如下一行:

defaultConfig {  
    // default setup here

    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

这样我们就完成了所有配置,让我们开始使用 Espresso 编写验收测试吧。

编写验收测试

想必你应该注意到了 androidTest 文件夹在 src 目录下:Espresso 测试通常就写在这里。创建一个名为 TeamsActivityTest 的类,然后添加一对注解如下所示:

@RunWitch(AndroidJunit4.class)
@LargeTest
public class TeamsActivityTest {
}

我们在代码中声明了将会使用 JUnit 4 来编写测试。@LargeTest注解向测试执行器指示了该类包含什么类型的测试,它经常在 Espresso 测试中出现。下面我们将会在测试类中添加以下字段:

@Rule public ActivityTestRule<TeamsActivity> activityTestRule = new ActivityTestRule<>(TeamsActivity.class);

在 JUnit 4 引入之前 Android 测试类通常继承自 ActivityInstrumentationTestCase2。使用 JUnit 4 并在测试类中添加被 @Rule 注解的 ActivityTestRule类型的字段就足以描述待测 Activity 如何被启动了。查看 ActivityTestRule 的几个构造方法找到合适测试启动的那个。

让我们继续在我们的测试类中实现测试用例:

@Test 
public void teamsListIsSortedAlphabetically() {  
    onView(withId(android.R.id.list)).check(matches(isSortedAlphabetically()));
}

onView()withId()matches() 都是框架中的静态方法,所以我建议使用静态导入来时测试定义看起来简洁明了。在 GitHub 中的 示例代码中查看中学的导入。

isSortedAlphabetically() 是一个自定义的 Hamcrest 匹配器,描述了我们想检查我们的 View,换句话说,检查 android.R.id.list 中的内容是否按字母顺序排序。下面是匹配器的定义:

private static Matcher<View> isSortedAlphabetically() {  
    return new TypeSafeMatcher<View>() {

        private final List<String> teamNames = new ArrayList<>();

        @Override
        protected boolean matchesSafely(View item) {
            RecyclerView recyclerView = (RecyclerView) item;
            TeamsAdapter teamsAdapter = (TeamsAdapter) recyclerView.getAdapter();
            teamNames.clear();
            teamNames.addAll(extractTeamNames(teamsAdapter.getTeams()));
            return Ordering.natural().isOrdered(teamNames);
        }

        private List<String> extractTeamNames(List<Team> teams) {
            List<String> teamNames = new ArrayList<>();
            for (Team team : teams) {
                teamNames.add(team.name);
            }
            return teamNames;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("has items sorted alphabetically: " + teamNames);
        }
    };
}

由于我们知道使用的是 RecyclerView,所以我们可以安全的转换 matchesSafely() 的参数,并取出 TeamsAdapter 以得到其中的数据。我们使用 extractNames() 方法从列表中取出 Team 对象的名称,然后使用 Guava 的 Ordering 类检查列表是否正确的排序。编写 Hamcrest 匹配器时,不要忽视 describeTo() 方法,它在测试失败时非常有用。在我们的 describeTo() 中,我们简短的描述了匹配器做了什么并会打印我们保存的数据:现在,当测试失败时,我们将会明确知道数据集合是什么样的并得出测试失败的原因。

现在,你可能会有疑问:TeamTeamAdapter(或我们还没有集成的 RecyclerView)来自哪里呢?编写测试,甚至不编译是非常好的测试驱动开发(TDD)方式。该方式引入了“红-绿-重构”循环:编写测试,使它们编译通过,重构以提出重复代码。我们现在在“红”阶段,接下来让我们编写一些代码进入“绿”阶段。

首先,通过在 app/build.gradle 中添加以下依赖集成 RecyclerView

dependencies {  
    // other "compile" dependencies go here
    compile "com.android.support:recyclerview-v7:${APPCOMPAT_VERSION}"

    // "androidTest" dependencies are here
}

如果在你的工程中已经有了一个 MainActivity,请将它重命名为 TeamsActivity,或者直接创建。TeamsActivity 将使用这个布局Team 是我们的实体类(POJO),代码如下所示:

public class Team {

    public final String name;
    public final @DrawableRes int logoRes;

    public Team(@NonNull String name, @DrawableRes int logoRes) {
        this.name = name;
        this.logoRes = logoRes;
    }

    public static final Comparator<Team> BY_NAME_ALPHABETICAL = new Comparator<Team>() {
        @Override public int compare(Team lhs, Team rhs) {
            return lhs.name.compareTo(rhs.name);
        }
    };
}

请注意 BY_NAME_ALPHABETICAL 比较器——我们将使用它来按需排序 Team 对象。

下面是 TeamAdapter 类,简洁易懂:

public class TeamsAdapter extends RecyclerView.Adapter<TeamsAdapter.ViewHolder> {

    private final LayoutInflater layoutInflater;

    private final List<Team> teams;

    public TeamsAdapter(LayoutInflater layoutInflater) {
        this.layoutInflater = layoutInflater;
        this.teams = new ArrayList<>();
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ViewHolder(layoutInflater.inflate(R.layout.row_team, parent, false));
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Team team = teams.get(position);
        holder.teamLogo.setImageResource(team.logoRes);
        holder.teamName.setText(team.name);
    }

    @Override public int getItemCount() {
        return teams.size();
    }

    public void setTeams(List<Team> teams) {
        this.teams.clear();
        this.teams.addAll(teams);
        notifyItemRangeInserted(0, teams.size());
    }

    public List<Team> getTeams() {
        return Collections.unmodifiableList(teams);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        ImageView teamLogo;
        TextView teamName;

        public ViewHolder(View itemView) {
            super(itemView);
            teamLogo = (ImageView) itemView.findViewById(R.id.team_logo);
            teamName = (TextView) itemView.findViewById(R.id.team_name);
        }
    }
}

row_team 的布局在这里。现在,让我们添加代码来为 TeamActivity 创建 Team 对象并初始化 TeamAdapter

@Override
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    RecyclerView teamsRecyclerView = (RecyclerView) findViewById(android.R.id.list);
    teamsRecyclerView.setLayoutManager(new LinearLayoutManager(this));

    TeamsAdapter teamsAdapter = new TeamsAdapter(LayoutInflater.from(this));
    teamsAdapter.setTeams(createTeams());
    teamsRecyclerView.setAdapter(teamsAdapter);
}

private List<Team> createTeams() {  
    List<Team> teams = new ArrayList<>();
    String[] teamNames = getResources().getStringArray(R.array.team_names);
    TypedArray teamLogos = getResources().obtainTypedArray(R.array.team_logos);
    for (int i = 0; i < teamNames.length; i++) {
        Team team = new Team(teamNames[i], teamLogos.getResourceId(i, -1));
        teams.add(team);
    }
    teamLogos.recycle();
    Collections.sort(teams, Team.BY_NAME_ALPHABETICAL);
    return teams;
}

我们在把列表传入适配器之前使用 Team.BY_NAME_ALPHABETICAL 适当的进行排序。

请通过 GitHub上的示例将代码补全。

代码编写完了!现在你可以通过右击 TeamsActivityTest 类选择“Run”命令来执行测试,也可以在命令行中执行如下命令:

./gradlew connectedAndroidTest

测试会执行通过,就算测试失败,通常我们也会得到 Espresso 非常有用的输出信息来帮助我们调试问题。

现在,我们使用 Espresso 编写了回归测试,该测试会自动检查我们已经实现的功能是否正常。如上文所说,GitHub上有完整的示例代码。

你有使用 Espresso 编写测试验证你的功能吗?希望能与你交流。如果你有任何反馈或发现文中错误,欢迎留言或直接与我联系,祝好!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,359评论 6 343
  • Instrumentation介绍 Instrumentation是个什么东西? Instrumentation测...
    打不死的小强qz阅读 7,684评论 2 39
  • 虞美人·春花秋月何时了 【作者】李煜【朝代】五代 春花秋月何时了?往事知多少。小楼昨夜又东风,故国不堪回首月明中。...
    丨冰之心丨阅读 155评论 0 0