如何在 Grails 单元测试中使用 Spock 模拟 passwordEncoder

Posted

技术标签:

【中文标题】如何在 Grails 单元测试中使用 Spock 模拟 passwordEncoder【英文标题】:How to mock passwordEncoder using Spock in a Grails unit test 【发布时间】:2020-07-22 12:19:50 【问题描述】:

我可以使用一些建议来模拟 Grails 单元测试中使用的自动连接依赖项。我已经省略了大部分不必要的代码,只给出了测试类和被测文件类中的相关方法

class UserService 

    def springSecurityService // spring bean
    def passwordEncoder // auto wired as per 
    //https://***.com/questions/33303585/spring-//security-encode-password-with-       bcrypt-algorithm

.....

    def passwordPreviouslyUsed(String newPassword, def userId)
        def passwordExists = false
        def usersPasswords = findPasswordsForUser(userId)
        usersPasswords.each password ->
            if (passwordEncoder.isPasswordValid(oldPassword, newPassword, null)) 
                passwordExists = true
            
        
        return passwordExists
    

    .....
    def findPasswordsForUser(def userId)
        User foundUser = User.findById(userId)
        def passwordsForUser = UserPasswords.createCriteria().list 
            eq('user', foundUser) 
            projections
                property('password')
            
        
        passwordsForUser
    

我的测试

class UserServiceSpec extends Specification implements DataTest, ServiceUnitTest<UserService> 

    def passwordEncoder
    def setupSpec() 
        mockDomains User, UserPasswords
    

    def setup() 
        def stubPasswordEncoder =  Stub(passwordEncoder) 
            isPasswordValid(_, _, _) >> true
         
         service.passwordEncoder = stubPasswordEncoder
    


    void "test for user passwordPreviouslyUsed"() 
        given: "a user already exists"
        setup()
        service.createNewUser("testName", "testy@test.com", "Secret1234" )
        //^(does some validation, then User.save())
        User foundUser = User.findByEmail("testy@test.com")
        foundUser.fullName == "testName"
        long testUserId = foundUser.id

        and: "we update the password for that user, and it to the userPasswords"
        UserPasswords newUserPassword = new UserPasswords(
            user: foundUser,
            password: "newPassword1"
        )
        newUserPassword.save()

        //use passwordPreviouslyUsed method to check a string with the same value as the 
        //previously 
        //updated password to check if it has already been used
        when: "we check if the password has been used before"

        def response = service.passwordPreviouslyUsed("newPassword1", fundsyId)

        then:
        response == true
    

没有存根或模拟这个依赖,我得到错误

Cannot invoke method isPasswordValid() on null object

我尝试存根密码编码器并让它返回 true

def stubPasswordEncoder =  Stub(passwordEncoder) 
    isPasswordValid(_, _, _) >> true
 
 service.passwordEncoder = stubPasswordEncoder

但这给出了一个错误信息:

Stub in 'spock.mock.MockingApi' cannot be applied to         '(java.lang.Object, groovy.lang.Closure)'

有没有办法用 Spock 模拟这种依赖关系?

【问题讨论】:

【参考方案1】:

Stub 和 Mock 接受一个类 - 你给它一个为 null 的实例 - 因此是异常。

你应该可以这样模拟它:

def mockPasswordEncoder = Mock(PasswordEncoder) 
// note this is the class 
// org.springframework.security.crypto.password.PasswordEncoder

【讨论】:

此外,不要从您的 given: 块或测试中的其他任何地方调用 setup()。 Spock 负责拨打setupSpec()setup()cleanup()cleanupSpec()。它甚至可以确保如果基本规范和子类规范都有自己的固定方法,它们不会被覆盖,而是按定义的顺序调用。【参考方案2】:

我尝试了enrichelgeson 的方法,它奏效了! 我首先将 PasswordEncoder 导入到测试类中

import org.springframework.security.crypto.password.PasswordEncoder

然后执行正常的模拟程序。我一开始很困惑,因为他们被测类只是通过定义隐式地创建了一个类的实例。

def stubPasswordEncoder =  Stub(PasswordEncoder) 
        isPasswordValid(_, _, _) >> true
    
    service.passwordEncoder = stubPasswordEncoder

我还找到了另一个不需要模拟的解决方案

service.passwordEncoder = [ isPasswordValid:  String rawPass, String salt, Null -> true  ]

这两种方法都可以正常工作。感谢您的帮助!

【讨论】:

以上是关于如何在 Grails 单元测试中使用 Spock 模拟 passwordEncoder的主要内容,如果未能解决你的问题,请参考以下文章

Grails Spock单元测试需要模拟事务管理器

如何使用 Spock 测试 Grails 服务?

如何在 Spock 控制器测试中模拟 Grails 请求对象方法

使用 Spock 进行 Grails 测试 - 选择哪个模拟框架?

Grails 2.3 使用 IVY 解析器进行单元测试

Grails / Spock:如何在类中模拟从类本身调用方法的单个方法?