Бесполезно издеваться над поведением интерфейса, если его не вызвать в тесте - PullRequest
1 голос
/ 18 января 2020

Нужно ли имитировать интерфейсы, которые не вызывают, например, поле имени пользователя и пароля пустое? Сначала я пытаюсь написать тест, но не понимаю, нужно ли использовать макеты.

Мой тест входа в систему

   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? хотя они не вызываются, так как имя пользователя и / или поля пусты?

Это последняя версия метода входа в систему, которую я намереваюсь написать после тестов

  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
}

Что-то не так с таким поведенческим тестированием?

1 Ответ

1 голос
/ 28 января 2020

Я бы посоветовал вам не проверять в тесте «Ошибка пустых полей: результат пустых полей». Я бы также предложил вам написать отдельные тесты для каждого пустого поля. Если вы выполняете строгий TDD, вы будете тестировать каждое условие при написании кода. то есть «Пустое имя пользователя при ошибке» будет первым тестом и первым проверенным условием, затем «Пустой пароль должен быть ошибкой» следующим (после того, как вы выполнили два отдельных написанных вами второго теста, ваш код может выглядеть как

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 в ваш класс (внедрение конструктора) и тогда вам нужно будет начать имитировать вызовы по мере их использования. Как правило, фальшивые фреймворки будут имитировать объект (т. е. код будет выполняться, но не даст никакого значимого результата). Вам следует стремиться возвращать фактические фиктивные данные, когда вы хотите протестировать конкретное c поведение, т. е. вы должны возвращать действительный объект из authRepository.login при тестировании на успешный вход в систему. Обычно я не использую методы настройки в @BeforeEach и использую либо фабричный метод, либо строитель, чтобы создать мой класс под тестом. Я не знаком с синтаксисом 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);
}

Вам нужно будет посмотреть, как создать Builder в kotlin, но конечный результат, который вы бы искали, заключается в том, что Builder всегда начинает устанавливать зависимости для тестируемого вами класса как mocks, которые ничего не делают, но позволяют вам изменять эти mock.

например

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

И последнее. Я специально исключил обсуждение проверки loginAttempt выше, так как для меня это выглядит так, как будто AuthUseCase - это класс обслуживания, который будет использоваться несколькими пользователями и действителен в течение всей жизни запроса, и в этом случае вы не хотите поддерживать состояние внутри класса ( т.е. переменная loginAttempt имеет то же время жизни, что и класс). Было бы лучше записать количество попыток на имя пользователя в таблице базы данных, и после каждого успешного входа необходимо будет сбрасывать счетчик попыток.

Надеюсь, это поможет.

...