【问题标题】:Mocked java class in spock test is not being executedspock测试中的模拟java类没有被执行
【发布时间】:2018-03-15 14:44:35
【问题描述】:

我正在尝试使用 spock 框架对一些 java 类进行单元测试。结构如下:

  • com.myorg.requests(类名:RequestProcessor
  • com.myorg.query(类名:DatabaseQuery

第一个类看起来像这样:

public class RequestProcessor {

   private String request;

   public RequestProcessor(String aRequest) {
      this.request = request;
   }

   public String processRequest() {
       String response ;
       //do something here

       try {
           if(condition meets) {
              response = executeRequest();
           }
       } catch ( various exceptions... ) {
           System.out.println("something went wrong...");
       }
   }

   private String executeRequest() throws <<exceptions thrown by DatabaseQuery>> {
       //do something here
       DatabaseQuery queryResult = new DatabaseQuery(request)
   }
}

我正在尝试为这个依赖于DatabaseQueryRequestProcessor 类编写Spock 测试。我正在考虑模拟 DatabaseQuery 类,以便单独测试 RequestProcessor 类。

正在调用RequestProcessorprocessRequest() 方法,该方法依赖于另一个私有方法。该方法将使用DatabaseQuery 来获取实际的查询结果。这就是我的 Spock 测试的样子:

class RequestProcessorSpec extends Specification {

    //Class to be tested
    RequestProcessor requestProcessor

    //Dependencies
    DatabaseQuery dbquery

    def "Given a valid request, dbquery's executeQuery method is called" () {
        given: "a valid request"
            def queryRequest = '{"info1":"value1","info2":"value2","query":"select * from users"}'

        and: "mock the DBQuery class"
            dbquery = Mock(DatabaseQuery)

        and: "create a new request"
            requestProcessor = new RequestProcessor(queryRequest)

        when: "the request is processed"
            requestHandler.processRequest()

        then: "dbquery executeQuery method is called"
            1 * dbquery.executeQuery(_ as String)
    }
 }

这并不完全适合我。我收到一个错误:

  • 调用太少
  • 不匹配的调用

当我使用gradlew test --info 运行测试以获得更多结果时,我看到控制台上打印了一条日志,该日志由processRequest 方法中的try-catch 语句捕获。

我在这里做错了什么?

【问题讨论】:

    标签: java unit-testing mocking spock


    【解决方案1】:

    示例代码中的问题

    首先,您的示例代码不起作用,即使我简化它并创建自己的虚拟DatabaseQuery 类,因为您在这里至少有三个错误:

    • 在构造函数中有this.request = request(自赋值),但它应该是this.request = aRequest;
    • 在测试中你有requestHandler.processRequest(),但应该是requestProcessor.processRequest()
    • 方法executeRequest() 不返回指定的String。所以我只能推测,实际上它在DatabaseQuery上调用了另一个方法,从而将查询结果转换为String

    为什么模拟不起作用?

    解决了这个问题,让我们看看你的测试有什么根本性的问题。

    我在这里做错了什么?

    假设本地变量中的模拟在应用程序代码中的其他本地变量中以某种方式有效。为了使用模拟,您需要将其注入到被测类中。但是像许多开发人员一样,您没有通过依赖注入来设计解耦和可测试性,而是在内部创建依赖项——在本例中为 DatabaseQuery 对象。

    测试还有什么问题?

    我认为您的测试只是过度规范。为什么要检查另一个类中的特定方法是否从私有方法调用(间接调用)?您不直接测试私有方法,而是通过调用公共方法来覆盖它们的代码,而您的测试已经这样做了。

    如果您想覆盖您的 catch 块,只需确保您的请求会导致正确的异常。也许您需要为此模拟数据库连接并确保它将预期结果返回给DatabaseQuery。我没有看到足够的代码来准确地说。

    原问题的技术解决方案

    现在让我们假设您绝对想检查这种交互,不管我之前说过什么。你需要做什么取决于情况(你没有在你的代码中显示):

    在任何情况下,您都需要使DatabaseQuery 可注入。你可以在你的类中添加一个成员和另一个 setter。

    现在你有一个岔路口,这取决于交互 dbquery.executeQuery(_ as String) 是从哪里进行(调用)的:

    1. 如果从外部DatabaseQuery调用该方法,则可以注入一个普通的Mock
    2. 如果从 DatabaseQuery 内部调用该方法,则需要注入 Spy,因为模拟不会像原始对象那样调用其他内部方法,因为 - 好吧,它只是一个模拟。

    案例1:executeQuery(String)被外部调用

    package de.scrum_master.query;
    
    public class DatabaseQuery {
      private String request;
    
      public DatabaseQuery(String request) {
        this.request = request;
      }
    
      public String executeQuery(String request) {
        return request.toUpperCase();
      }
    
      public String getResult() {
        return executeQuery(request);
      }
    }
    
    package de.scrum_master.requests;
    
    import de.scrum_master.query.DatabaseQuery;
    
    public class RequestProcessor {
      private String request;
      private DatabaseQuery databaseQuery;
    
      public RequestProcessor(String aRequest) {
        this.request = aRequest;
        databaseQuery = new DatabaseQuery(request);
      }
    
      public String processRequest() {
        return executeRequest();
      }
    
      private String executeRequest() {
        return databaseQuery.executeQuery(request);
        //return databaseQuery.getResult();
      }
    
      public void setDatabaseQuery(DatabaseQuery databaseQuery) {
        this.databaseQuery = databaseQuery;
      }
    }
    
    package de.scrum_master.requests
    
    import de.scrum_master.query.DatabaseQuery
    import spock.lang.Specification
    
    class RequestProcessorTest extends Specification {
      //Class to be tested
      RequestProcessor requestProcessor
    
      //Dependencies
      DatabaseQuery dbquery
    
      def "Given a valid request, dbquery's executeQuery method is called" () {
        given: "a valid request"
        def queryRequest = '{"info1":"value1","info2":"value2","query":"select * from users"}'
    
        and: "mock the DBQuery class"
        dbquery = Mock(DatabaseQuery)
        //dbquery = Spy(DatabaseQuery, constructorArgs: [queryRequest])
    
        and: "create a new request"
        requestProcessor = new RequestProcessor(queryRequest)
        requestProcessor.databaseQuery = dbquery
    
        when: "the request is processed"
        requestProcessor.processRequest()
    
        then: "dbquery executeQuery method is called"
        1 * dbquery.executeQuery(_ as String)
      }
    }
    

    现在测试工作包括交互检查。

    案例 2:executeQuery(String) 在其自身的类内部调用

    您是否看到RequestProcessorRequestProcessorTest 中的两条注释掉的行?只需使用它们并像这样注释掉其他两个:

      private String executeRequest() {
        //return databaseQuery.executeQuery(request);
        return databaseQuery.getResult();
      }
    
        and: "mock the DBQuery class"
        //dbquery = Mock(DatabaseQuery)
        dbquery = Spy(DatabaseQuery, constructorArgs: [queryRequest])
    

    测试仍然有效,包括交互检查。

    当然,我不得不伪造一些东西并填补你没有提供的缺失拼图,但这基本上是它的工作原理。

    【讨论】:

    • 感谢您花时间回复和解释。如您所知,我没有在这里复制完整的代码。这就是我发布示例后出现一些错误的原因。我需要花一些时间来阅读您的解释和示例。只是写信说谢谢!
    • 欢迎。应该不会花太多时间,因为我的示例代码是完整的MCVE,包括包和类名、测试和应用程序示例代码。只需复制、粘贴并运行。
    • 这样做的正确方法是什么? RequestProcessor 类只有 1 个公共方法。该方法的成功调用取决于同一类上的私有方法,该方法实例化另一个类(DatabaseQuery)。对于负面测试场景,我想我可以检查公共方法调用(processRequest)是否遇到任何异常,但是,如何正确测试依赖于私有方法的快乐路径?另外,我真的应该让“databaseQuery”成为一个全局变量,即使它只被一种特定的私有方法使用吗?
    • 我不明白你的问题。我没有像您所说的那样使数据库查询成为 “全局变量”。它是一个私有成员变量。我只是添加了一个注入它的选项。你有没有读过依赖注入作为一种设计模式?如果你不喜欢 setter,你也可以使用构造函数注入。无论如何,我想我已经回答了最初的问题,正如我的代码所示。如果您有任何新问题,我希望您接受并支持它并创建一个新问题。因为对于“这样做的正确方式”,我确实需要更多信息,而不仅仅是 sn-ps。
    • 再次感谢您的解释。这很有帮助。我需要重新评估当前的实现。
    猜你喜欢
    • 1970-01-01
    • 2021-11-28
    • 1970-01-01
    • 2016-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-22
    • 1970-01-01
    相关资源
    最近更新 更多