【问题标题】:Spock with DbUnit test cases slownessSpock 与 DbUnit 测试用例缓慢
【发布时间】:2018-01-12 14:16:56
【问题描述】:

我们已将 Spock + Db unit 框架作为自动化单元测试的一部分。

我们现在有 2000 个测试用例(功能),用于 DbUnit 的 150 个规范

在这里,我们在数据库中添加所需的条目,然后测试每个方法的行为。

我们观察到执行这些测试用例大约需要 2 小时 30 分钟。

我已为 setup 夹具添加时间戳,并在功能方法中添加了时间戳。以下是我的观察:

allergy.dao.AllergyFormDAOSpec > Get Allergy Form STANDARD_OUT
    setup method execution started at : Fri Jan 12 19:00:42 IST 2018

allergy.dao.AllergyFormDAOSpec > API to get Allergy Form STANDARD_OUT
Feature method execution started at : Fri Jan 12 19:00:44 IST 2018
Feature method execution ended at: Fri Jan 12 19:00:45 IST 2018
Total time taken to run the one test case: 242

cleanup method execution started at : Fri Jan 12 19:00:45 IST 2018
Total time taken to run a feature method : 2531

在这里,我观察到在设置后加载功能方法平均需要 2-4 秒。但是,原始测试用例的执行时间不到一秒。

我想知道我是否可以得到关于这里可能延迟的指示?因为,2000 个测试用例的 3 秒意味着 Spock 花费了几乎 1 小时和 30 分钟的时间,而不是真正的功能执行。

总而言之,我们希望减少 Spock 测试用例在日常运行时所花费的总时间。

规格

package allergy.dao

import java.util.Date

import org.dbunit.IDatabaseTester;
import org.dbunit.ext.mssql.InsertIdentityOperation;

import allergy.AllergyForm;
import be.janbols.spock.extension.dbunit.DbUnit;
import spock.lang.Shared
import util.MasterSpec

class AllergyFormDAOSpec extends MasterSpec {
    def dao = new AllergyFormDAO();
    @Shared Date timeStart1
    @Shared Date timeEnd1

@DbUnit(configure={ IDatabaseTester it ->
    it.setUpOperation = InsertIdentityOperation.REFRESH
    it.tearDownOperation = InsertIdentityOperation.DELETE
})
def content =  {
    allergy_form(formId:99999,formName:'DummySpockForm',displayIndex:1,deleteFlag:0,is_biological:1)
    allergy_form_facilities(id:99999,formId:99999,facilityid:2)
    form_concentration(id:99999,formId:99999,name:'1:100',deleteflag:0,displayindex:1)
}

def setup(){
    timeStart1 = new Date()
    println "setup method execution started at : " +  timeStart1
}

def "API to test delete Form facility"(){
    def startTime = new Date()
    println "Feature method execution started at : " +  startTime
    given:"form Id is given"
        def formId = 99999
    when:"delete form facilities"
        def result =dao.deleteFormFacilities(null, formId)
    then:"validate result"
        (result>0)==true
        def endTime = new Date()
        println "Feature method execution ended at: " +  endTime
        println 'Total time taken to run the one test case: '+ (endTime.getTime() - startTime.getTime())
}

def cleanup() {
    timeEnd1 = new Date()
    println "cleanup method execution started at : " +  timeEnd1

    def difference = timeEnd1.time - timeStart1.time
    println "Total time taken to run a fixture method : " + difference
}
}

MasterSpec

package util

import com.ecw.dao.SqlTranslator
import catalog.Root
import spock.lang.Shared
import spock.lang.Specification

import javax.sql.DataSource

/**

 */
class MasterSpec extends Specification {

@Shared
Properties properties = new Properties()
@Shared
public DataSource dataSource
@Shared
protected xmlDataSource = [:]

static int timeCntr = 0;

//setup is to read xml file's content in xmlDataSource Hashmap
def setup(){

    //Get Running Class name without its package
    def className = this.class.name.substring(this.class.name.lastIndexOf('.') + 1)
    def resourceAnno = specificationContext.currentFeature.featureMethod.getAnnotation(FileResource)

    if(resourceAnno != null){
        def files = resourceAnno.xmlFiles()
        def packageName = (this.class.package.name).replaceAll('\\.','/')

        for(int i=0;i< files.length;i++){
            def f = new File("src/test/resources/"+packageName+"/"+className+"/"+files[i])
            def engine = new groovy.text.GStringTemplateEngine()
            def template = engine.createTemplate(f).make(null)
            def xmlString = template.toString()

            //load the hashmap with file name as Key and its content in form of string as Value
            xmlDataSource.put(files[i].split("\\.")[0],xmlString)
        }
    }
}

def setupSpec() {
    Date timeStart = new Date()

    File propertiesFile = new File('src/test/webapps/myApp/conf/connection.properties').withInputStream {
        properties.load it
    }

    String strDBName = getPropertyValue("myApp.DBName")
    if(strDBName.indexOf('?') > -1){
        strDBName = strDBName.substring(0, strDBName.indexOf('?'))
    }
    String strServerName = getPropertyValue("myApp.DBHost");
    if(strServerName.indexOf(':') > -1){
        strServerName = strServerName.substring(0, strServerName.indexOf(':'))
    }
    String strUrl = getPropertyValue("myApp.DBUrl")
    String strPort = strUrl.substring(strUrl.lastIndexOf(':') + 1)

    //FOR MSSQL
    System.setProperty("myApp.SkipJndi", "yes")
    //dataSource = new JtdsDataSource()
    Object newObject = null;
    if(SqlTranslator.isDbSqlServer()){
        newObject = Class.forName("net.sourceforge.jtds.jdbcx.JtdsDataSource").newInstance()
    } else if(SqlTranslator.isDbMySql()){
        newObject = Class.forName("com.mysql.jdbc.jdbc2.optional.MysqlDataSource").newInstance()
    }

    dataSource = (DataSource)newObject
    dataSource.setDatabaseName(strDBName)
    dataSource.setUser(getPropertyValue("myApp.DBUser"))
    dataSource.setPassword(getPropertyValue("myApp.DBPassword"))
    dataSource.setServerName(strServerName)
    dataSource.setPortNumber(Integer.parseInt(strPort))

}
}

【问题讨论】:

  • 没有任何代码我们只能胡乱猜测。
  • 一位经验丰富且信誉评分高的会员提出的如此模糊的问题令人难以置信。我认为您应该知道如何就 SO 提出问题。
  • @kriegaex:我可以发布代码。但是,我试图了解 Spock 的生命周期,或者是否有人经历过这样的经历。让我放下整个规范..

标签: unit-testing spock dbunit


【解决方案1】:

这个问题确实太宽泛了,没有提供足够的信息来给出一个合格的答案。但是,我可以说 Spock 本身应该非常快(不如“原始”JUnit 快,毕竟它仍然很时髦,但对于测试来说确实足够快)。

从您的问题来看,您似乎怀疑 Spock 是一个瓶颈,所以, 例如,您可以在启用了 DbUnit 的测试旁边放置一个空的 Spock 测试并测量其执行情况,我可以向您保证,您获得的时间可以忽略不计。

所以我相信原因是在设置/清理期间,DbUnit 调用了一些与数据库相关的代码(可能是模式生成/表填充和/或删除),这需要很多时间。所以我的第二次尝试只是打印在测试期间运行的 SQL 查询,您可能会发现其中许多是在 setup 方法期间运行的。

另一个可能的原因是为了进行测试,在测试之前插入了太多数据。

另一个可能的原因是您运行测试的数据库太慢(网络慢,数据库本身太忙)。

现在有什么办法可以解决这一切? :) 您可能想看看 Spring 测试数据访问层的方法 + 如何进行初始设置。既然远远超出了问题的范围,这里就不过多讲spring了,只是作为一个想法:

  • 生成一次数据
  • 在所有测试运行之前插入它
  • 随着测试的开始,开始一个事务
  • 在测试完成时回滚事务(即使测试成功),这样数据就不会被保留。如果您使用 Isolation 并且数据库服务器支持它,您甚至可以并行运行测试,这没问题。

如果缓慢的原因是数据库服务器,那么(除了“更改您的 RDBMS”之类的明显建议之外)您可以尝试在同一台机器上的 docker 中运行数据库/甚至在本地开始测试之前使用TestContainers.

【讨论】:

  • 我们也尝试过不使用 DbUnit 测试用例。尽管如此,设置后运行功能方法仍然需要时间。此外,它适用于所有规格。我添加了一些代码
  • 你能测量主规范的设置和设置规范执行时间吗?可能他们会指出/成为自己缓慢的核心原因?一般来说 setup/setupSpec 就像 JUnit 中的 @Before/@BeforeClass 一样,没有什么特别之处,也许除了 setupSpec 不像 JUnit 对应物那样不是静态的......
  • 我们没有增加太多的差异。但是,看起来 Spock 并不是这里的瓶颈。我们正在研究我们的 Mocking 和 DbUnit 实现
  • 我们注意到,如果我们删除“@PrepareForTest”注释,所有测试用例都会在中场休息时间开始运行。我会发表我的分析。我发现你的答案更接近我的问题。
【解决方案2】:

为了分析这个问题的根本原因,我们确实在以下情况下跑了一个差事:

1) 1000 个没有任何 Db 或模拟依赖项的 Spock 测试用例 (PowerMock)

解释场景的示例代码:

package mathOperations;

import groovy.lang.Closure
import mathOperations.Math
import spock.lang.Specification

class MathSpec extends Specification {
    def objMath =new Math()

    def "API to test addition of two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.AddNumber is called with given values"
            def result =objMath.addNumber(a,b)
        then: "Result should be 15"
            result==15
    }

    def "API to test subrtaction of two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.subtractNumber is called with given values"
            def result =objMath.subtractNumber(a, b)
        then: "Result should be 5"
            result==5
    }

    def "API to test multiplication of two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.multiplyNumber is called with given values"
            def result =objMath.multiplyNumber(a,b)
        then: "Result should be 50"
            result==50
    }
    def "API to test division two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.divisionNumber is called with given values"
            def result =objMath.divisionNumber(a,b)
        then: "Result should be 2"
            result==2
    }

    def "API to test whether given both numbers are equal - Affirmative"() {
        given :"a and b"
            def a=10
            def b=10
        when: "Math.equalNumber is called with given values"
            def result =objMath.equalNumber(a,b)
        then: "It should return true"
            result==true
    }

    def "API to test whether given both numbers are equal - Negative"() {
        given :"a and b"
            def a=10
            def b=11
        when: "Math.equalNumber is called with given values"
            def result =objMath.equalNumber(a,b)
        then: "It should return false"
            result==false
    }
}

--> 花费了 25.153 秒,包括构建时间,下面是下面的报告

2) 1000 个带模拟的 Spock 测试用例 (PowerMock)

解释场景的示例代码:

package mathOperations;

import groovy.lang.Closure
import mathOperations.Math
import spock.lang.Specification
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.rule.PowerMockRule
import org.junit.Rule
import utils.QRCodeUtils
import org.powermock.api.mockito.PowerMockito
import static org.powermock.api.mockito.PowerMockito.mockStatic
import static org.mockito.BDDMockito.*

@PrepareForTest([QRCodeUtils.class])
class MathSpec extends Specification {
    def objMath =new Math()

    @Rule PowerMockRule powerMockRule = new PowerMockRule()

    def "API to test add two numbers"() {
        given :"a and b"
        def a=10
        def b=5
        when: "Math.AddNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.addNumber(a,b)
        then: "result should be 15"
        result==15
    }

    def "API to test subract two numbers"() {
        given :"a and b"
        def a=10
        def b=5
        when: "Math.subtractNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.subtractNumber(a, b)
        then: "result should be 5"
        result==5
    }
    def "API to test multiple two numbers"() {
        given :"a and b"
        def a=10
        def b=5
        when: "Math.multiplyNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.multiplyNumber(a,b)
        then: "result should be 50"
        result==50
    }
    def "API to test divide two numbers"() {
        given :"a and b"
        def a=10
        def b=5
        when: "Math.divisionNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.divisionNumber(a,b)
        then: "result should be 2"
        result==2
    }
    def "API to test modulo of a number"() {
        given :"a and b"
        def a=10
        def b=5
        when: "Math.moduloNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.moduloNumber(a,b)
        then: "result should be 0"
        result==0
    }

    def "API to test power of a number"() {
        given :"a and b"
        def a=10
        def b=2
        when: "Math.powerofNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.powerofNumber(a,b)
        then: "result should be 0"
        result==8
    }

    def "API to test numbers are equal -affirmative"() {
        given :"a and b"
        def a=10
        def b=10
        when: "Math.equalNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.equalNumber(a,b)
        then: "It should return true"
        result==true
    }

    def "API to test numbers are equal -negative"() {
        given :"a and b"
        def a=10
        def b=11
        when: "Math.equalNumber is call with given values"
        mockGetOTPAttemptStatus(5)
        def result =objMath.equalNumber(a,b)
        then: "It should return false"
        result==false
    }

    void mockGetOTPAttemptStatus(int status) {
        mockStatic(QRCodeUtils.class)
        when(QRCodeUtils.getOTPAttemptStatus(anyInt())).thenReturn(status)
    }
}

--> 花了 9 分钟 14.222 秒 包括构建时间,下面是报告

3) 仅使用 Dbunit 的 1000 个 Spock 测试用例。 (通常,我们在一个测试用例中平均插入 15-20 个表条目。这里,我们添加了相同的)

解释场景的示例代码:

package mathOperations;

import groovy.lang.Closure
import java.sql.Statement
import mathOperations.Math
import spock.lang.Shared
import util.BaseSpec
import catalog.Root
import spock.lang.Ignore
import org.dbunit.ext.mssql.InsertIdentityOperation
import be.janbols.spock.extension.dbunit.DbUnit
import org.dbunit.IDatabaseTester

class MathSpec extends BaseSpec {

    @Shared root
    def objMath =new Math()

    @DbUnit(configure={
        IDatabaseTester it ->
        it.setUpOperation = InsertIdentityOperation.REFRESH
        it.tearDownOperation = InsertIdentityOperation.DELETE
    })
    def content =  {
        table1(id:99,MasterFile:'UnitTestFile',DataElementName:'test',DataElementDBColName:'TestDbCol',DataElementTableName:'TestTable')
        table2(id:99,MasterFile:'UnitTestFile',DataElementName:'test',DataElementDBColName:'TestDbCol',DataElementTableName:'TestTable')
        table3(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
        table4(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
        table5(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
        table6(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
        table7(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
        table8(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
        table9(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
        table10(id:99,Code:'T00.0',Status:'A',LongDesc:'Unit Testing')
    }

    def "API to test addition of two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.AddNumber is called with given values"
            def result =objMath.addNumber(a,b)
        then: "Result should be 15"
            result==15
    }

    def "API to test subrtaction of two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.subtractNumber is called with given values"
            def result =objMath.subtractNumber(a, b)
        then: "Result should be 5"
            result==5
    }

    def "API to test multiplication of two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.multiplyNumber is called with given values"
            def result =objMath.multiplyNumber(a,b)
        then: "Result should be 50"
            result==50
    }
    def "API to test division two numbers"() {
        given :"a and b"
            def a=10
            def b=5
        when: "Math.divisionNumber is called with given values"
            def result =objMath.divisionNumber(a,b)
        then: "Result should be 2"
            result==2
    }

    def "API to test whether given both numbers are equal - Affirmative"() {
        given :"a and b"
            def a=10
            def b=10
        when: "Math.equalNumber is called with given values"
            def result =objMath.equalNumber(a,b)
        then: "It should return true"
            result==true
    }

    def "API to test whether given both numbers are equal - Negative"() {
        given :"a and b"
            def a=10
            def b=11
        when: "Math.equalNumber is called with given values"
            def result =objMath.equalNumber(a,b)
        then: "It should return false"
            result==false
    }
}

--> 花费了 57 分钟 18.136 秒,包括构建时间,以下是报告

我们得出的结论是,Spock 不需要花费时间来运行测试用例,但执行检测 SO Link 和 DbUnit(使用反射加载所有内容)的 Power Mock 是瓶颈。

解决方案: 我们将使用内部框架来插入/删除数据库数据,而不是 DbUnit。此外,我们已将 Power Mock 替换为 JMockit

最终结果:正如我在问题中发布的那样,总时间从 2 小时 30 分钟,现在已减少到 6 分钟对于这 1000 个测试用例。 :)

【讨论】:

    【解决方案3】:

    答案其实很简单:您忘记测量主规格的设置时间。也许您假设子规范中的 setup() 方法会覆盖其父规范的 setup() 方法。但在 Spock 中却没有!全部

    • setupSpec()
    • setup()
    • cleanup()
    • cleanupSpec()

    类层次结构中的所有规范将按以下顺序执行:首先是基类,然后是子类。

    让我用一个原始的例子告诉你我在说什么:

    主要规格:

    为什么是线程局部静态变量?好吧,也许您正在同时运行测试。对于这个简单的示例,它是不必要的。

    package de.scrum_master.stackoverflow
    
    import spock.lang.Specification
    import static System.currentTimeMillis
    
    class MasterSpec extends Specification {
      static ThreadLocal<Long> startMillis = new ThreadLocal<>()
    
      def setupSpec() {
        startMillis.set(currentTimeMillis())
        sleep 50
        println "BaseSpec.setupSpec: " + (currentTimeMillis() - startMillis.get())
      }
    
      def cleanupSpec() {
        sleep 50
        println "BaseSpec.cleanupSpec: " + (currentTimeMillis() - startMillis.get())
      }
    
      def setup() {
        sleep 50
        println "BaseSpec.setup: " + (currentTimeMillis() - startMillis.get())
      }
    
      def cleanup() {
        sleep 50
        println "BaseSpec.cleanup: " + (currentTimeMillis() - startMillis.get())
      }
    }
    

    派生规范:

    为什么叫DerivedTest?仅仅因为我的 Maven 构建配置为根据默认名称 *Test(Surefire 单元测试)或 *IT(故障安全集成测试)查找测试。

    package de.scrum_master.stackoverflow
    
    import spock.lang.Unroll
    
    import static java.lang.System.currentTimeMillis
    
    class DerivedTest extends MasterSpec {
      def setupSpec() {
        sleep 50
        println "DerivedTest.setupSpec: " + (currentTimeMillis() - startMillis.get())
      }
    
      def cleanupSpec() {
        sleep 50
        println "DerivedTest.cleanupSpec: " + (currentTimeMillis() - startMillis.get())
      }
    
      def setup() {
        sleep 50
        println "DerivedTest.setup: " + (currentTimeMillis() - startMillis.get())
      }
    
      def cleanup() {
        sleep 50
        println "DerivedTest.cleanup: " + (currentTimeMillis() - startMillis.get())
      }
    
      @Unroll
      def "feature #id"() {
        given:
        long featureStartMillis = currentTimeMillis()
        sleep 50
        println "DerivedTest.feature $id: " + (currentTimeMillis() - startMillis.get())
    
        expect:
        true
    
        cleanup:
        println "DerivedTest.feature $id ONLY: " + (currentTimeMillis() - featureStartMillis)
    
        where:
        id << ["A", "B"]
      }
    }
    

    控制台日志:

    BaseSpec.setupSpec: 105
    DerivedTest.setupSpec: 193
    BaseSpec.setup: 286
    DerivedTest.setup: 336
    DerivedTest.feature A: 396
    DerivedTest.feature A ONLY: 55
    DerivedTest.cleanup: 453
    BaseSpec.cleanup: 504
    BaseSpec.setup: 556
    DerivedTest.setup: 606
    DerivedTest.feature B: 656
    DerivedTest.feature B ONLY: 50
    DerivedTest.cleanup: 706
    BaseSpec.cleanup: 757
    DerivedTest.cleanupSpec: 808
    BaseSpec.cleanupSpec: 858
    

    你能看到执行的顺序以及每个步骤所消耗的时间吗?

    我想说的是,您在主规范中的复杂操作(读取配置文件、初始化和填充数据库等)

    【讨论】:

    • 我们正在使用 powermock 通过使用 @PrepareForTest 注释来模拟静态方法。会不会也是瓶颈之一?stackoverflow.com/questions/21846183/…
    • 也许,但也许不是。我之前使用过 Powermock 来连接 Spock,但从未注意到那里有任何性能问题。不过我不再使用 PM 了,因为如果你使用它,它是一种气味,并且是一个明确的信号,表明你应该重构你的代码以使其可测试。除此之外,我无法相信您似乎忽略了我对您的问题的解释,并且仍在其他地方寻找问题。问题就在键盘面前!请再次阅读我的分析和示例代码,并与您自己的代码进行比较。很明显,是你的代码让测试变慢了。
    • 更新:我刚刚打开了一个旧的 Powermock 测试并添加了时序语句。在我的笔记本电脑上,使用 @PrepareForTest + @Rule PowerMockRulesetup() 结束和功能方法开始之间的规范的平均时间是 290 毫秒,即延迟很明显,但远不及 2 秒。我建议您克隆您的测试并注释掉越来越多的代码部分,直到您看到时间浪费在哪里。
    • 我们注意到,如果我们删除“@PrepareForTest”注释,所有测试用例都会在中场休息时间开始运行。我会发表我的分析。目前,我们已将 Mocking 更改为 jMockito,因为我们需要模拟私有和静态方法。感谢您的时间和回答
    • 我不知道 jMockito。不过,很高兴得知它对您来说更快。对于静态我使用 PowerMock(好吧,或者更确切地说我重构),对于私人 Spock 就足够了,不需要使用额外的工具。对于 final 类和方法,您也可以使用 PowerMock,但必须使用它的断言,或者使用 special test runner,这使您能够继续使用 Spock 的普通模拟、存根和间谍。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-09
    • 2019-12-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-09
    • 2017-06-12
    • 1970-01-01
    相关资源
    最近更新 更多