使用 Spock 和 Robospock 创建 SQLite 数据库的单元测试

Posted

技术标签:

【中文标题】使用 Spock 和 Robospock 创建 SQLite 数据库的单元测试【英文标题】:Unit testing creating an SQLite database using Spock and Robospock 【发布时间】:2014-09-14 03:14:56 【问题描述】:
spock-core:0.7-groovy-2.0
robospock:0.5.0
android Studio 0.8.2
Fedora release 20 (Heisenbug)

这是完整的解决方案。现在它编译并运行单元测试成功,目录结构与预览编辑相同。请随时对任何看起来不正确的地方发表评论。

编辑解决方案 =====

build.gradle:

apply plugin: 'java'
apply plugin: 'groovy'

repositories 
    mavenCentral()

    maven 
        // Location of Android SDK for compiling otherwise get this error:
        /* Could not find com.android.support:support-v4:19.0.1.
           Required by:
           :testSQLite:unspecified > org.robospock:robospock:0.5.0 > org.robolectric:robolectric:2.3 */
        url "/home/steve/local/android-studio/sdk/extras/android/m2repository/"
    


dependencies 
    // just compile so we can use the sqlite API
    compile 'com.google.android:android:4.1.1.4', 
        // Do not bring in dependencies
        transitive = false
    

    testCompile 'org.codehaus.groovy:groovy:2.3.+'
    testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'
    testCompile 'org.robospock:robospock:0.5.0'
    testCompile 'org.robospock:robospock-plugin:0.4.0'

SnapzClientTest.groovy:

package com.example.DataAccess

import com.example.DataAccess.SnapzAndroidDB

import org.robolectric.Robolectric
import pl.polidea.robospock.RoboSpecification

class SnapClientTest extends RoboSpecification 

    /* Create Sqlite database for Android */
    def 'Create a sqlite database for Android'() 
        setup:
        def androidDB = new SnapzAndroidDB(Robolectric.application)

        expect:
        androidDB != null
    

SnapzAndroidDB.java,与 8 月 5 日编辑相比没有变化

Edit 5 August ================

基本上,我正在尝试创建一个将在具有SQLite 功能的Android 应用程序中使用的JAR 文件,因此我可以将这个JAR 文件用于许多应用程序。

我从头开始创建了一个更小的应用程序,更容易修复错误。这是目录结构,只有三个文件:

testSQLite/build.gradle
testSQLite/src/main/java/com/example/sqltest/SnapzAndroidDB.java
testSQLite/src/test/groovy/SnapzClientTest.groovy

build.gradle

apply plugin: 'java'
apply plugin: 'groovy'

repositories 
    mavenCentral()

    maven 
        // Location of Android SDK for compiling otherwise get this error:
        /* Could not find com.android.support:support-v4:19.0.1.
           Required by:
           :testSQLite:unspecified > org.robospock:robospock:0.5.0 > org.robolectric:robolectric:2.3 */
        url "/home/steve/local/android-studio/sdk/extras/android/m2repository/"
    


dependencies 
    // Just compile so we can use the sqlite API
    compile 'com.google.android:android:4.1.1.4', 
        // Do not bring in dependencies
        transitive = false
    

    testCompile 'org.codehaus.groovy:groovy:2.3.+'
    testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'
    testCompile 'org.robospock:robospock:0.5.0'
    testCompile 'org.robospock:robospock-plugin:0.4.0'

SnapzAndroidDB.java

package com.example.DataAccess;

import java.util.logging.ConsoleHandler;
import java.util.logging.SimpleFormatter;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.logging.Level;

import android.content.Context;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteException;
import android.database.Cursor;

public class SnapzAndroidDB extends SQLiteOpenHelper 
    /**
     * Logger for displaying log messages
     */
    private static final Logger log = Logger.getLogger("SnapzAndroidDB");

    private SQLiteDatabase mDb;

    public SnapzAndroidDB(Context context) 
        super(context, "DB_NAME", null, 1);

        /* Create logger */
        ConsoleHandler consoleHandler = new ConsoleHandler();
        log.addHandler(consoleHandler);
        log.setLevel(Level.FINE);
        consoleHandler.setFormatter(new SimpleFormatter());
        consoleHandler.setLevel(Level.ALL);

        log.log(Level.INFO, "SnapzAndroidDB()");
    

    /* Called only once first time the database is created */
    @Override
    public void onCreate(SQLiteDatabase mDb) 
        log.log(Level.INFO, "onCreate(SQLiteDatabase db)");

        String createConfig = String.format("create table %s (%s int primary key, %s text, %s text)",
                                         "TABLE_CONFIG",
                                         "ID",
                                         "NAME",
                                         "VALUE");

        log.log(Level.INFO, "onCreate with SQL: " + createConfig);
        mDb.execSQL(createConfig);
    

    @Override
    public void onUpgrade(SQLiteDatabase mDb, int oldVersion, int newVersion) 
        log.log(Level.INFO, "onUpgrade()");
        /* Only if there is some schema changes to the database */
    

SnapzClientTest.groovy

package com.example.DataAccess

import com.example.DataAccess.SnapzAndroidDB

import spock.lang.Specification
import org.robolectric.Robolectric

class SnapClientTest extends Specification 


    /* Create SQLite database for Android */
    def 'Create an SQLite database for Android'() 
        setup:
        def androidDB = new SnapzAndroidDB(Robolectric.application)

        expect:
        androidDB != null
    

我仍然遇到的错误如下:

com.example.DataAccess.SnapClientTest > Create an SQLite database for Android FAILED
    java.lang.RuntimeException: Stub!
        at android.database.sqlite.SQLiteOpenHelper.<init>(SQLiteOpenHelper.java:4)
        at com.example.DataAccess.SnapzAndroidDB.<init>(SnapzAndroidDB.java:26)
        at com.example.DataAccess.SnapClientTest.Create a sqlite database for Android(SnapzClientTest.groovy:15)

8 月 4 日编辑 ====================

这是我更新的测试规范,它使用 Robolectric 生成可在 SQLiteOpenHelper(...) 的构造函数中使用的上下文

import org.robolectric.Robolectric

def 'Create an SQLite database for Android'() 
    setup:
    def androidDB = new SnapzAndroidDB(Robolectric.application)

    expect:
    androidDB != null

我实际测试的函数是一个扩展SQLiteOpenHelper 的类。而我的构造函数SnapzAndroidDB(...) 调用SQLiteOpenHelper() 构造函数,你可以看到上下文是从测试规范传递的第一个参数:

public class SnapzAndroidDB extends SQLiteOpenHelper
    public SnapzAndroidDB(Context context) 
        super(context, SnapzContract.DB_NAME, null, SnapzContract.DB_VERSION);       
    
    .
    .

当我运行我的测试时,我得到了这个错误:

com.sunsystem.HttpSnapClient.SnapClientTest > Create an SQLite database for Android FAILED
    java.lang.RuntimeException: Stub!
        at android.database.sqlite.SQLiteOpenHelper.<init>(SQLiteOpenHelper.java:4)
        at com.sunsystem.DataAccess.SnapzAndroidDB.<init>(SnapzAndroidDB.java:33)
        at com.sunsystem.HttpSnapClient.SnapClientTest.Create a sqlite database for Android(SnapClientTest.groovy:168)

结束编辑 ========================

编辑 ====

当我尝试使用 getBaseContext() 时,出现以下错误:

com.sunsystem.HttpSnapClient.SnapClientTest > Create an SQLite database for Android FAILED
    groovy.lang.MissingMethodException: No signature of method: com.sunsystem.HttpSnapClient.SnapClientTest.getBaseContext() is applicable for argument types: () values: []
        at com.sunsystem.HttpSnapClient.SnapClientTest.Create a sqlite database for Android(SnapClientTest.groovy:159)

我的规范 spock 函数是这样的:

def 'Create an SQLite database for Android'() 
    setup:
    def androidDB = new SnapzAndroidDB(getBaseContext())

    expect:
    androidDB != null

以下是依赖项:

dependencies 
    compile "com.googlecode.json-simple:json-simple:1.1.1", 
        // Exclude junit as we don't want this include in our JAR file as it will add hamcast and other dependencies as well
        exclude group:'junit', module: 'junit'
    

    // Just compile so we can use the SQLite API. This won't be included in the JAR
    compile 'com.google.android:android:4.1.1.4', 
        // Do not bring in dependencies
        transitive = false
    

    // Compile for unit testing only
    testCompile "org.codehaus.groovy:groovy:2.3.4"
    testCompile "org.spockframework:spock-core:0.7-groovy-2.0"
    testCompile 'org.robospock:robospock:0.5.0'
    testCompile 'com.google.android:android-test:4.1.1.4'
    testCompile 'com.android.tools.build:gradle:0.12.2'
    testCompile 'org.robospock:robospock-plugin:0.4.0'

====

我正在为我用 Java 编写的库进行 Spock 单元测试,该库将在我的 Android 应用程序中使用。

将部署到 Android 应用程序以执行数据库操作的 Java JAR 文件。我正在测试的就是这个 JAR 文件。

我已经编写了一个 Spock 规范来测试 SQLite 数据库的创建。

在我的 Java JAR 文件中,我有一个创建 SQLite 数据库的类,我想在我的 Spock 单元测试中对其进行测试。

但是,问题在于 SQLiteOpenHelper 构造函数需要使用 Context 调用,而我正在尝试在 Spock 单元测试中使用 import android.text.mock.MockContext 模拟该上下文。

public class SnapzAndroidDB extends SQLiteOpenHelper implements SnapzDAO 
    public SnapzAndroidDB(Context context) 
        super(context, SnapzContract.DB_NAME, null, SnapzContract.DB_VERSION);    
    

    /* Called only once first time the database is created */
    @Override
    public void onCreate(SQLiteDatabase db) 
        String sqlCreate = String.format("create table %s (%s int primary key, %s text, %s text, %s text)",
                                         SnapzContract.TABLE,
                                         SnapzContract.GetConfigColumn.ID,
                                         SnapzContract.GetConfigColumn.NAME,
                                         SnapzContract.GetConfigColumn.VALUE,
                                         SnapzContract.GetConfigColumn.CFG_TYPE);
        db.execSQL(sqlCreate);
    
    .
    .

现在在我的单元测试规范中,我的 SnapClientTest.groovy 中有这个:

import android.test.mock.MockContext

def 'Create an SQLite database for Android'() 
    setup:
    def context = new MockContext()
    def androidDB = new SnapzAndroidDB(context.getApplicationContext())

    expect:
    androidDB != null

从这里你可以看到我正在模拟上下文并将其作为参数发送到我的类的构造函数,该构造函数将调用 SQLiteOpenHelper 构造函数。

我在运行单元测试时遇到的错误是这样的:

com.HttpSnapClient.SnapClientTest > Create an SQLite database for Android FAILED
11:05:27.062 [DEBUG] [TestEventLogger]     java.lang.RuntimeException: Stub!
11:05:27.063 [DEBUG] [TestEventLogger]         at android.content.Context.<init>(Context.java:4)
11:05:27.063 [DEBUG] [TestEventLogger]         at android.test.mock.MockContext.<init>(MockContext.java:5)
11:05:27.063 [DEBUG] [TestEventLogger]         at com.sunsystem.HttpSnapClient.SnapClientTest.Create a sqlite database for Android(SnapClientTest.groovy:155)
11:05:27.065 [QUIET] [system.out] 11:05:27.064 [DEBUG] [org.gradle.process.internal.child.ActionExecutionWorker] Stopping client connection.

作为 Spock 的新手,我不确定这是否可行,因为我只是在测试我的 JAR 文件。

【问题讨论】:

使用 Robolectric.application 作为上下文是否解决了问题? 我更新了我的答案,你应该使用 RoboSpecification 而不是 Specification。 【参考方案1】:

Spock 是 Groovy 和 Java 生态系统中使用最广泛的框架之一,它允许以非常直观的语言创建 BDD 测试并促进一些常见任务,例如模拟和可扩展性。使它在人群中脱颖而出的是其美丽且极具表现力的规范语言。由于其 JUnit 运行器,Spock 与大多数 IDE、构建工具和持续集成服务器兼容。要使用 Spock,您基本上需要执行一组步骤,例如遵循 recipe,这将使您能够有效地实现单元测试和 Web 集成。

您当前的错误消息为:

Create a sqlite database for Android FAILED

试试这些步骤,看看效果如何:

在您的代码中包含 getWritableDatabase 和 getReadableDatabase 应该会有所帮助:

jokesHelper dbHelper = new jokesHelper(getBaseContext());
SQLiteDatabase db = dbHelper.getWritableDatabase();

这样做,Android 将能够管理和缓存connection。

但是,如果您从 getBaseContext 收到任何错误消息,请尝试卸载测试插件并使用 --eclipse 集成重新创建 STS 资源(.classpath 和 .project),那么它应该 @ 987654326@。

如果您对 getSpecificationContext 有任何问题,则表示遗漏了一些细节,您需要仔细检查您的 specifications。

如果你不使用Eclipse,用spock创建你的Java Jar文件,你可以通过命令行Java development工具将它与Emacs接口为usual,例如Sun的JDK或任何其他方法预计为Enterprise Development。要仅运行 SampleTest,您必须使用 Java system 属性从命令行调用测试任务:

gradle -Dtest.single=Sample test

或者

gradle -Dtest.single=SoapTest clean test

还要检查正在使用的目录上的permissions 是什么。如果您还没有完成,请记得添加dependencies:

dependencies 
    classpath 'com.android.tools.build:gradle:0.8.+'
    classpath 'org.robospock:robospock-plugin:0.4.0'

并通知测试目录你是using,例如源目录。请记住 ("Es ist wichtig das man hier die richtige Resources-Klasse importiert") 导入课程所需的 right resource 很重要。同样,include 也可以在“defaultConfig”中的“build.gradle”:

testPackageName "com.yourpackage.test"
testInstrumentationRunner "android.test.InstrumentationTestRunner"
testFunctionalTest true

Spock 和 Robospock 是创新工具,可为 unit test 的开发提供有用的资源。或者,您也可以使用 TCL Tests 等工具。 TCL 测试是SQLite 最古老的测试集,是您可以采用的最佳方法。事实上,SQLite 最初是作为 Tcl 扩展的。 SQLite 的许多测试和开发工具都是用 Tcl 编写的。除了原生 C API,Tcl 扩展是核心 SQLite 团队唯一支持的 API。

要启用 Tcl 绑定,请从 SQLite website 下载 SQLite 源的 TEA(Tcl 扩展架构)分发版。此版本的代码本质上是与 Tcl 绑定附加到末尾。这将构建到一个 Tcl 扩展中,然后可以将其导入到任何 Tcl 环境中。

应遵循非常具体的步骤,注意每一个detail 至关重要,因为它可以决定您的测试能否成功运行。

instrumentation framework 是测试框架的基础。 Instrumentation 控制被测应用程序并允许注入应用程序运行所需的模拟组件。例如,您可以在应用程序启动之前创建模拟上下文并让应用程序使用它们。

应用程序与周围环境的所有交互都可以使用这种方法进行控制。您还可以将您的应用程序隔离在受限环境中,以便能够预测结果,强制某些方法返回的值或模拟 ContentProvider、数据库甚至文件系统内容的持久和未更改的数据。因此,在您的活动中指定您正在运行test 的信息也很重要:

<?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.aatg.sample.test"
        android:versionCode="1" android:versionName="1.0">
        <application android:icon="@drawable/icon"
            android:label="@string/app_name">
            <uses-library android:name="android.test.runner" />
        </application>
        <uses-sdk android:minSdkVersion="7" />
        <instrumentation
            android:targetPackage="com.example.aatg.sample
            android:name="android.test.InstrumentationTestRunner"
            android:label="Sample Tests" />
        <uses-permission android:name="
            android.permission.INJECT_EVENTS" />
    </manifest>

如果您运行 JNI 以使用本机代码操作数据库,有两种方法可以使用 SQLite 加载扩展。一种是通过 C API 调用,另一种是通过 SQL 函数调用与 C API 函数相同的代码。在这两种情况下,您都需要提供文件名,并且可以选择提供入口点函数的名称:

int sqlite3_load_extension( sqlite3 *db, const char *ext_name,
    const char *entry_point, char **error )

加载可加载扩展的另一种方法是使用内置的SQL function:

load_extension( 'ext_name' )
load_extension( 'ext_name', 'entry_point' )

此函数类似于 C sqlite3_load_extension() 调用,但有一个主要限制。因为这是一个 SQL 函数,所以在调用它时,根据定义,加载扩展时会执行一条 SQL 语句。这意味着使用 load_extension() SQL 函数加载的任何扩展都将完全无法重新定义 或删除自定义函数,包括专用的 like() 函数集。使用suitable syntax 加载数据的方法与Java 类似,与expected 一样。

调试指令仅用于测试和开发 目的,因为它们会增加大量开销并使一切运行速度明显变慢,就像包含 throws Exception 一样。当您运行单元测试时,您需要设置它们accordingly,并检查以避免您的数据库没有得到corrupted。基本上,为您的调试设置实现最佳调整将改善并帮助您以最佳方式顺利进入put your testing to run。

除了所有其他构建指令,SQLite 有相当数量的 SQLITE_OMIT_* 编译时指令。这些旨在从构建中删除核心功能,以使核心数据库库尽可能小和紧凑。为了使用这些省略指令中的大部分,您需要从源代码控制树中的开发源构建 SQLite。大多数省略指令都不起作用 正确应用于源分发或预构建合并时。另请注意,这些编译时指令不受官方支持,因为它们不是官方测试链的一部分。对于任何给定版本的 SQLite,如果启用了任意的省略标志集,则可能同时存在编译问题和运行时问题。

当然,您无需成为 samurai 即可在 Android 上为 SQLite 运行单元测试,尽管它可能会有所帮助。

【讨论】:

谢谢,这不是我正在寻找的答案。我目前正在尝试使用 spock 框架来解决,因为我们所有的单元测试都使用了这个。 我确实尝试使用 getBaseContext(),但出现错误。请参阅我编辑的问题。我也尝试过使用 getSpecificationContext(),但这也失败了。谢谢。 如果这是可能的,我不是,因为我最后一次尝试失败了。只是另一件事,我没有使用 Eclipse,而是使用 Emacs 使用 gradle 和 spock 创建我的 Java Jar 文件。谢谢。【参考方案2】:

您面临的问题是获取创建数据库的正确上下文。

您第一次尝试 getBaseContext() 没有成功,因为它在 SnapClientTest 中找不到类似的函数:“没有方法签名”

在您的第二次尝试中,您正在创建一个 MockContext 实例 - 这是一个无法直接使用的存根实现。

http://developer.android.com/reference/android/test/mock/MockContext.html “一个模拟 Context 类。所有方法都不起作用并抛出 UnsupportedOperationException。”

试试:

def androidDB = new SnapzAndroidDB(Robolectric.application)

根据http://robospock.org/ Robolectric.application 应该给出一个工作上下文。

更新 我刚刚注意到您不是在扩展 RoboSpecification 而是扩展规范:

import pl.polidea.robospock.RoboSpecification

class SnapClientTest extends RoboSpecification

【讨论】:

嗨 Raanan,使用 Robolectric.application 让我更进一步。但是,SQLiteOpenHelper 在我试图传递给它的构造函数中抛出异常,在我的测试中是不正确的。 SQLiteOpenHelper 需要“用于打开或创建数据库的上下文”这是错误消息:java.lang.RuntimeException: Stub!在 android.database.sqlite.SQLiteOpenHelper.(SQLiteOpenHelper.java:4) 在 com.sunsystem.DataAccess.SnapzAndroidDB.(SnapzAndroidDB.java:33) SnapzAndroidDB.java 是我调用 SQLiteOpenHelper 并通过的地方上下文。谢谢 您好,请在使用 Robolectric.application 时将问题更新为当前状态,包括引发异常的代码和堆栈跟踪。 (从你的评论中看不太清楚)。就个人而言,赏金前往一个没有回答甚至试图回答问题的答案有点烦人。 当您看到类似“Stub!”的内容时,您将什么上下文传递给 SQLiteOpenHelper这意味着您传递的上下文不是完整的上下文实现。 我已经用我的测试规范和我正在测试的函数以及堆栈跟踪更新了我的答案。如果我能给你解决这个问题的全部赏金,那么我会的。我不确定为什么赏金部分去了另一个答案。我传递的上下文将是由 Robolectric.application 生成的上下文。 你什么时候应用 Robospock 插件?是在Android插件之后吗?检查构建脚本:robospock.org .... 在 Robolectric 中,此错误通常在未应用测试运行程序时出现,即“存根!”错误来自仅使用“android.jar”,它没有实现所有类,因为它假定它们是在真实设备上实现的。

以上是关于使用 Spock 和 Robospock 创建 SQLite 数据库的单元测试的主要内容,如果未能解决你的问题,请参考以下文章

Spock-忽略子类的规范方法

Spock 模拟验证返回 0 次调用

Spock单元测试框架实战指南十 - 注意事项

如何使用 spring boot 和 spock 运行测试容器

Spock 和 Spring Boot 集成测试

如何使用 gradle 6+ 和 java 11+ 在 Spring Boot 中配置 spock