Search code examples
unit-testingtddmockk

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


Do i need to mock interfaces that does not call, for instance user name and password field is empty? I'm trying to write test first but confused if mocks should be used.

My login 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
}

Do i have to mock interface for the given part of the test or AccountManager even though they are not called since user name and/or fields are empty?

This is the final version of login method i intend to write after tests

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
        }
    
    }

Should i only mock when all other states and passed i get a mock response from repository and interaction with account manager occurs?

What should be given section of the test?

Edit:

I updated given section of this test to

@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
}

Is there something wrong with this kind of behavior testing?


Solution

  • I would suggest that you don't need the verify in the "Empty fields result empty fields error" test. I would also suggest you write separate tests for each empty field. If you were doing strict TDD you would be testing each condition as you wrote the code. i.e. 'Empty username should error" would be the first test and the first condition tested, then "Empty password should error" the next (after you have done two separate written your second test your code may look like

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

    Once both the tests above pass you could refactor to

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

    Once you start testing the conditional statements for checkUserNameIsValid and checkIfPasswordIsValid, you would need to introduce the authRepository and accountManager to your class (constructor injection) and then you would need to start mocking the calls as you use them. Generally mocking frameworks will fake an object (i.e. the code will run but won't return any meaningful result). You should aim to return actual mock data when you want to test specific behavior i.e. you should be returning a valid object from the authRepository.login when you are testing for a successful login. Generally I stay away from using setup methods in the @BeforeEach and use either a factory method or builder to create my class under test. I am unfamiliar with the kotlin syntax so can at best do some sudo code to demonstrate how your builder or factory functions may look like.

    // 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);
    }
    

    You will need to have a look at how to create a builder in kotlin but the end result you would be looking for is that the builder always starts setting the dependencies for you class under test as mocks that do nothing but allows you to change those mocks.

    e.g.

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

    One final thing. I purposely left out discussing loginAttempt check above as to me it looks like the AuthUseCase is a service class that will be used by multiple users and live for the lifetime of the request in which case you don't want to maintain state within the class (i.e. the loginAttempt variable has the same lifetime as the class). It would be better to record the attempts per username in a database table and the attempt count would need to be reset after each successful login.

    Hope this helps.