androidmockitoamazon-cognitojunit4aws-amplify-sdk-android

Mocking AWS Amplify Auth APIs for Android


My Android application uses AWS Cognito and Amplify Auth SDK for authentication and I'm trying to write JUnit test cases for login/signup flows. I'm using Mockito framework to mock the classes.

I started with login, my login model looks like this

class LoginService(val auth: AuthCategory) {

 fun login(username: String, password: String): MutableLiveData<Login> {
    val liveData = MutableLiveData<Login>()
    auth.signIn(username, password,
        { result ->
            liveData.postValue(Login(result, null))
        },
        { error ->
            liveData.postValue(Login(null, error))
        }
    )
    return liveData
    }
  }

And my viewmodel calls it this way

class LoginViewModel : ViewModel() {

    val loginService = LoginService(Amplify.Auth)

    fun login(username: String, password: String): MutableLiveData<Login> {
        return loginService.login(username, password)
    }
}

And my test case looks like this

lateinit var auth: AuthCategory
lateinit var loginService: LoginService

@Before
fun onSetup() {
    auth = mock(Amplify.Auth::class.java)
    loginService = LoginService(auth)
}

@Test
fun loginTest() {
    val authSignIn: Consumer<*>? = mock(Consumer::class.java)
    val authEx: Consumer<*> = mock(Consumer::class.java)
    `when`(
        auth.signIn(
            anyString(), anyString(),
            authSignIn as Consumer<AuthSignInResult>, authEx as Consumer<AuthException>
        )
    )
    loginService.login("username", "password").observeForever {
        assertTrue(it.result?.isSignInComplete!!)
    }
}

Please help me validate this approach, I'm trying to find out a way to trigger AuthSignInResult and AuthException of Auth.signIn() method so that I would assert if signin is successful or there is an error.

I'm very new to AWS Amplify and Cognito environment, A suggestion/reference to do this in correct way would be highly appreciated. Thanks in advance.


Solution

  • There are a couple ways you can encourage testability with Amplify Android. Of the two below, I would definitely start with the first approach.

    Use the category interfaces

    This is a "unit test" level approach.

    Amplify.Auth implements the AuthCategoryBehavior interface. So, if you change all of your code to use that interface, you can just mock it.

    Say you have some class which uses Auth as a dependency:

    class YourClass(private val auth: AuthCategoryBehavior = Amplify.Auth) {
        ...
    }
    

    Now, in your production code, you'd have your Dependency Injection code do something like this:

    1. Initialize Amplify (with addPlugin(AWSCognitoAuthPlugin()), Ampify.configure(...), etc.)
    2. Next, return a singleton your YourClass, built from YourClass(auth = Amplify.Auth)

    However in your test code, you can build an instance of YourClass using a mock of the Amplify Auth stuff:

    val mockAuth = mock(AuthCategoryBehavior::class.java)
    val yourClass = YourClass(mockAuth)
    

    With that, you can specify how it should behave under test conditions:

    doAnswer
        { invocation ->
            // Get a handle to the success callback
            val onResult =
                invocation.arguments[2] as Consumer<AuthSignInResult>
            // Invoke it with some canned result
            onResult.accept(mock(AuthSignInResult::class.java))
        }
        .`when`(mockAuth)
        .signIn(eq(username), eq(password), any(), any())
    

    Use OkHttp's MockWebServer

    This is more of a "component" or "integration" level approach. Here, we'll use a MockWebServer instance to return canned responses from a fake Cognito server.

    In this flow, you're using all of the real Amplify library code, in both production and in test. Just making believe you can control Cognito's responses to the client.

    To do this, you should view the actual HTTP responses in your Android Studio's Network Monitor tab. Then, arrange that content into the test code below.

    val mockWebServer = MockWebServer()
    mockWebServer.start(8080);
    val fakeCognitoEndpointUrl = mockWebServer.url("/");
    
    val cookedResponse = new MockResponse()
        .setResponseCode(200)
        .setBody(new JSONObject()
            .put("blah blah", "content you saw in Network Monitor")
            .toString()
        )
    mockWebServer.enqueue(cookedResponse)
    
    // Build up a JSON representation of your `amplifyconfiguration.json`
    // But replace the endpoint URL with mock web server's.
    val json = JSONObject()
        .put(...)
        // Find correct field to populate by
        // viewing structure of amplifyconfiguration.json
        .put("Endpoint", fakeCognitoEndpointUrl)
    
    val config = AmplifyConfiguration.fromJson(json)
    Amplify.addPlugin(AWSCognitoAuthPlugin())
    Amplfiy.configure(config, context)
    val yourClass = YouClass(auth = Amplify.Auth)
    

    Have left a few details unspecified in this second example, but hopefully it's enough to set you in a working direction.