【问题标题】:Unit Testing using spock mocking or grails mockFor: Null pointer exception使用 spock 模拟或 grails mockFor 的单元测试:空指针异常
【发布时间】:2015-03-11 17:42:16
【问题描述】:

我正在对我老板编写的一些代码进行单元测试。他在画一个空白,而我是 TDD 的新手,所以请和我一起集思广益。

我要测试的文件,EmailAssist 是此处未显示的服务的辅助类。如图所示,EmailAssist 应该引用其他几个服务,包括 sectionService。

   class EmailAssist {
       def sectionService
       //condensed to relevant items
       List<CommonsMultipartFile> attachments
       Map emailMap =[:]
       User user
       Boolean valid

       public EmailAssist(){
          valid = false
       }

       public EmailAssist(GrailsParameterMap params, User user){
              //irrelevant code here involving snipped items
              this.setSections(params.list('sections'))
              //series of other similar calls which are also delivering an NPE
       }
       //third constructor using third parameter, called in example but functionally 
       //similar to above constructor.

       //definition of errant function
       void setSections(List sections) {
            emailMap.sections = sectionService.getEmailsInSectionList(sections, user)
        }

正在调用的SectionService部分如下。

       Set<String> getEmailsInSectionList(List<String> sections, User user) {
            if(sections && user){
                //code to call DB and update list
            }
            else{
            []
            }

我的测试没有提供一个部分,所以这应该返回一个空列表,特别是因为我什至无法在单元测试中访问数据库。

单元测试如下。这是使用 mockFor,因为 spock 的模拟功能似乎不是我所需要的。

@TestMixin(GrailsUnitTestMixin)
class EmailAssistSpec extends Specification {
   @Shared
   GrailsParameterMap params
   @Shared
   GrailsMockHttpServletRequest request = new GrailsMockHttpServletRequest()
   @Shared
   User user
   @Shared
   def sectionService

   def setup() {
       user = new User(id: 1, firstName: "1", lastName: "1", username: "1", email: "1@1.com")
       def sectionServiceMock = mockFor(SectionService)
       sectionServiceMock.demand.getEmailsInSectionList() {
            []
       }
       sectionService = sectionServiceMock.createMock()
   }

   def cleanup(){
   }
   void testGetFiles(){
        when:
        //bunch of code to populate request

        params = newGrailsParameterMap([:], request)
        EmailAssist assist = new EmailAssist(params, request, user)
        //Above constructor call generates NPE

具体的 NPE 如下: java.lang.NullPointerException:无法在空对象上调用方法 getEmailsInSectionList() 在

        emailMap.sections = sectionService.getEmailsInSectionList(sections, user)

这是我的 setSections 函数的主体,适合那些在家玩的人。 NPE 堆栈源自我的测试文件中的构造函数调用。我也尝试过使用 spock 风格的模拟,但该服务仍然被认为是空的。最糟糕的是,构造函数甚至不是这个测试应该测试的,它只是拒绝传递它,结果导致测试无法运行。

如果我可以提供更多细节来澄清事情,请告诉我,谢谢!

编辑:我将构造函数中的设置器短路以完成测试,但这在测试覆盖率中留下了一些明显的漏洞,我无法弄清楚如何修复。也许我的嘲笑位于错误的地方? Spock 的模拟文档对于复杂的函数不是很方便。

【问题讨论】:

    标签: unit-testing grails groovy mocking spock


    【解决方案1】:

    您正在获取 NPE,因为 EmailAssist 中的 sectionService 没有被创建。 EmailAssist 的构造函数调用 sectionService.getEmailsInSectionList(sections, user) 并且因为 sectionService 为空,所以你得到一个 NPE。

    尽管您在测试setup() 中创建了一个名为sectionService 的模拟,但它不会自动连接/注入EmailAssist 类。您在 Grails 单元测试中获得的自动接线非常有限 - 文档对实际创建的 bean 不是很清楚(而且我对 Grails/Groovy 比较陌生)。

    您需要在创建emailAssist 时注入sectionService,否则逃脱NPE 为时已晚。

    如果您将单元测试中对构造函数的调用修改为:

    @TestMixin(GrailsUnitTestMixin)
    class EmailAssistSpec extends Specification {
       @Shared
       GrailsParameterMap params
       @Shared
       GrailsMockHttpServletRequest request = new GrailsMockHttpServletRequest()
       @Shared
       User user
       @Shared
       def mockSectionService = Mock(SectionService)
    
       def setup() {
           user = new User(id: 1, firstName: "1", lastName: "1", username: "1", email: "1@1.com")
       }
    
       def cleanup(){
       }
    
       void testGetFiles(){
            given: "an EmailAssist class with an overridden constructor"
    
            EmailAssist.metaClass.constructor = { ParamsType params, RequestType request, UserType user -> 
               def instance = new EmailAssist(sectionService: mockSectionService) 
               instance // this returns instance as it's the last line in the closure, but you can put "return instance" if you wish
            }            
    
            // note that I've moved the population of the request to the given section
            //bunch of code to populate request
            params = newGrailsParameterMap([:], request)
    
            // this is the list of parameters that you expect sectionService will be called with
            def expectedSectionList = ['some', 'list']
    
            when: "we call the constructor"
            EmailAssist assist = new EmailAssist(params, request, user)
    
            then: "sectionService is called by the constructor with the expected parameters"
            1 * mockSectionService.getEmailsInSectionList(expectedSectionList, user)
            // replace a parameter with _ if you don't care about testing the parameter
    

    此答案基于 Burt Beckwith here 的博客文章。

    【讨论】:

    • 我要看看这个。我真的只是出现说我发现这是问题所在,我会看看我是否可以让它工作并告诉你我想出了什么。
    • 没有 NPE,但似乎从未调用过 mockSectionService。即使我将其指定为“1 * _”,它也会抱怨调用太少。不知道该怎么想。
    • 仅在将近一周后详细说明:此解决方案在某种程度上起作用。目前尚不清楚有多少构造函数被覆盖,坦率地说,由于 metaClass 工作方式的挑剔,我们放弃了该实现。具体来说,元分类值与自身不一致。示例here
    猜你喜欢
    • 2018-02-22
    • 1970-01-01
    • 2010-11-15
    • 2020-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-05
    • 1970-01-01
    相关资源
    最近更新 更多