如果不在测试中调用,模拟接口的行为是否无用?

Is it useless to mock an interface's behavior if it's not to be called in the test

我是否需要模拟不调用的接口,例如用户名和密码字段为空?我正在尝试先编写测试,但对是否应该使用模拟感到困惑。

我的登录测试

private val authRepository: AuthRepository = mockk()

private val userManager: AccountManager = mockk()

private lateinit var authUseCase: AuthUseCase

@BeforeEach
fun setUp() {
    clearMocks(authRepository)
    clearMocks(userManager)
    authUseCase = AuthUseCase(authRepository, userManager)
} 

/**
 *  Scenario: Login check with empty fields:
 * * Given I am on the login page
 * * When I enter empty username
 *   And I enter empty password
 *   And I click on the "Login" button
 * * Then I get empty fields error.
 */
@Test
fun `Empty fields result empty fields error`() {
    // Given

    // When
    val expected = authUseCase.login("", "", false)

    // Then
    verify(exactly = 0) {
        authRepository.login(or(any(), ""), or(any(), ""), any())
    }
    expected assertEquals EMPTY_FIELD_ERROR
}

我是否必须为测试的给定部分模拟接口或 AccountManager 即使由于用户名 and/or 字段为空而未调用它们?

这是我打算在测试后编写的登录方法的最终版本

class AuthUseCase(
    private val authRepository: AuthRepository,
    private val accountManager: AccountManager
) {

    private var loginAttempt = 1
    /*
        STEP 1: Throw exception for test to compile and fail
     */
//    fun login(
//        userName: String,
//        password: String,
//        rememberMe: Boolean = false
//    ): AuthenticationState {
//        throw NullPointerException()
//    }


    /*
        STEP3: Check if username or password is empty
     */
//        fun login(
//        userName: String,
//        password: String,
//        rememberMe: Boolean = false
//    ): AuthenticationState {
//
//
//       if (userName.isNullOrBlank() || password.isNullOrBlank()) {
//           return EMPTY_FIELD_ERROR
//       }else {
//           throw NullPointerException()
//       }
//
//    }


    /**
     * This is the final and complete version of the method.
     */
    fun login(
        userName: String,
        password: String,
        rememberMe: Boolean
    ): AuthenticationState {

        return if (loginAttempt >= MAX_LOGIN_ATTEMPT) {
            MAX_NUMBER_OF_ATTEMPTS_ERROR
        } else if (userName.isNullOrBlank() || password.isNullOrBlank()) {
            EMPTY_FIELD_ERROR
        } else if (!checkUserNameIsValid(userName) || !checkIfPasswordIsValid(password)) {
            INVALID_FIELD_ERROR
        } else {

            // Concurrent Authentication via mock that returns AUTHENTICATED, or FAILED_AUTHENTICATION
            val authenticationPass =
                getAccountResponse(userName, password, rememberMe)

            return if (authenticationPass) {
                loginAttempt = 0
                AUTHENTICATED
            } else {
                loginAttempt++
                FAILED_AUTHENTICATION
            }
        }
    }
    
        private fun getAccountResponse(
            userName: String,
            password: String,
            rememberMe: Boolean
        ): Boolean {
    
            val authResponse =
                authRepository.login(userName, password, rememberMe)
    
            val authenticationPass = authResponse?.authenticated ?: false
    
            authResponse?.token?.let {
                accountManager.saveToken(it)
            }
    
            return authenticationPass
        }
    
    
        private fun checkUserNameIsValid(field: String): Boolean {
            return field.length >15 && field.endsWith("@example.com")
    
        }
    
        private fun checkIfPasswordIsValid(field: String): Boolean {
            return field.length in 6..10
        }
    
    }

我是否应该只在所有其他状态都通过时才模拟我从存储库获得模拟响应并与客户经​​理进行交互?

试题应该考什么?

编辑:

我将此测试的给定部分更新为

@Test
fun `Empty fields result empty fields error`() {

    // Given
    every { authRepository.login(or(any(), ""), or(any(), "")) } returns null

    // When
    val expected = authUseCase.login("", "", false)

    // Then
    verify(exactly = 0) { authRepository.login(or(any(), ""), or(any(), "")) }
    expected assertThatEquals EMPTY_FIELD_ERROR
}

这种行为测试有问题吗?

我建议您不需要在 "Empty fields result empty fields error" 测试中进行验证。我还建议您为每个空字段编写单独的测试。如果您执行严格的 TDD,您将在编写代码时测试每个条件。 IE。 “空用户名应该错误”将是第一个测试和第一个条件测试,然后 "Empty password should error" 下一个(在你完成两个单独编写的第二个测试之后,你的代码可能看起来像

if (userName.isNullOrBlank()) {
  return EMPTY_FIELD_ERROR
}
if (password.isNullOrBlank() {
  return EMPTY_FIELD_ERROR
}

一旦以上两个测试都通过,您就可以重构为

if (userName.isNullOrBlank() || password.isNullOrBlank()) {
            EMPTY_FIELD_ERROR
}

一旦您开始测试 checkUserNameIsValid 和 checkIfPasswordIsValid 的条件语句,您需要将 authRepository 和 accountManager 引入您的 class(构造函数注入),然后您需要在使用时开始模拟调用他们。通常模拟框架会伪造一个对象(即代码会 运行 但不会 return 任何有意义的结果)。当您想要测试特定行为时,您应该瞄准 return 实际模拟数据,即当您测试成功登录时,您应该 return 从 authRepository.login 中获取一个有效对象。通常,我不使用 @BeforeEach 中的设置方法,而是使用工厂方法或构建器来创建我的 class 待测。我不熟悉 kotlin 语法,所以最多可以做一些 sudo 代码来演示您的构建器或工厂函数的外观。

// overloaded factory function
fun create() {
  val authRepository: AuthRepository = mockk()
  val userManager: AccountManager = mockk()
  return AuthUseCase(authRepository, userManager);
}


fun create(authRepository: AuthRepository) {
  val userManager: AccountManager = mockk()
  return AuthUseCase(authRepository, userManager);
}


fun create(authRepository: AuthRepository, userManager: AccountManager) {
  return AuthUseCase(authRepository, userManager);
}

您将需要了解如何在 kotlin 中创建构建器,但您要寻找的最终结果是构建器总是开始为您设置依赖项 class 在测试中作为模拟什么也不做,只允许您更改这些模拟。

例如

AuthUseCase authUseCase = AuthUseCaseBuilder.Create().WithAuthRepository(myMockAuthRepository).Build();

最后一件事。我故意不讨论上面的 loginAttempt 检查,因为在我看来,AuthUseCase 是一项服务 class,它将被多个用户使用并在请求的生命周期内存在,在这种情况下,您不想保持状态在 class 内(即 loginAttempt 变量与 class 具有相同的生命周期)。最好在数据库中记录每个用户名的尝试次数 table 并且每次成功登录后都需要重置尝试次数。

希望对您有所帮助。