使用以下代码段,我一直在尝试使用Espresso(UiAutomator
自动填充用户输入字段)来使用发生登录的OpenID OAuth 2.0 library来测试登录身份验证流程通过自定义Chrome Tab意图在外部进行成功登录,从而通过启动Activity的onActivityResult()
回调将用户带回应用程序,然后再运行一些逻辑(通过验证下一个屏幕的视图是否在显示来确定屏幕的确发生了变化在这种情况下显示)。但是事实证明,该应用程序在登录后无法正确恢复,之后又引发了NoActivityResumedException
。
是的,我尝试使用Espresso-Intents
,但由于在测试登录屏幕中的整体登录流程时,我想尽办法测试{ {1}},特别是在按下登录按钮后触发其自己的意图(来自库的身份验证请求)。到目前为止,我感觉自己处在正确的轨道上,因此为我指明正确的方向提供任何帮助!
登录屏幕:
ActivityTestRule
助手身份验证功能:
class LoginActivity : AppCompatActivity() {
companion object {
const val RC_AUTH_LOGIN = 100
}
private lateinit var authService: AuthorizationService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
initAuthService()
initViews()
}
override fun onDestroy() {
authService.dispose()
super.onDestroy()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
RC_AUTH_LOGIN -> initViewModelAndObserve(data)
else -> // Display error message
}
}
}
private fun initAuthService() {
authService = AuthorizationService(this)
}
private fun initViews() {
start_auth_button?.setOnClickListener {
startAuthorization()
}
}
private fun initViewModelAndObserve(data: Intent?) {
// [authState] can either be retrieved from cache or [AuthState()]
AuthUtils.handleAuthorizationResponse(authService, data, authState) { success ->
if (success) {
// Run necessary API async calls and such within the ViewModel
// layer to observe.
loginViewModel.loginLiveData.observe(this, Observer<Boolean> { loginSuccessful ->
if (loginSuccessful) {
// Transition to the next screen
} else {
// Display error message
}
})
} else {
// Display error message
}
}
}
private fun startAuthorization() {
val req = AuthUtils.getAuthRequest()
val intent = authService.getAuthorizationRequestIntent(req)
startActivityForResult(intent, RC_AUTH_LOGIN)
}
}
Espresso UI测试:
object AuthUtils {
fun getAuthRequest(): AuthorizationRequest {
val authServiceConfig = getServiceConfig()
// [clientID], [redirectURI], and [clientSecret] dummy
// args.
val req = AuthorizationRequest.Builder(
authServiceConfig,
clientID,
ResponseTypeValues.CODE,
Uri.parse(redirectURI)
)
.setScope("scope")
.setPrompt("login")
.setAdditionalParameters(mapOf("client_secret" to clientSecret,"grant_type" to "authorization_code" ))
.build()
return req
}
fun handleAuthorizationResponse(authService: AuthorizationService,
data: Intent?,
appAuthState: AuthState,
resultCallBack: (result: Boolean) -> Unit) {
if (data == null) {
resultCallBack.invoke(false)
return
}
val response = AuthorizationResponse.fromIntent(data)
val error = AuthorizationException.fromIntent(data)
appAuthState.update(response, error)
if (error != null || response == null) {
resultCallBack.invoke(false)
return
}
val req = getTokenRequest(response)
performTokenRequest(authService, req, appAuthState) { authState ->
if (authState != null) {
authState.accessToken?.let { token ->
// For instance, decode token here prior to caching.
resultCallBack.invoke(true)
}
} else {
resultCallBack.invoke(false)
}
}
}
private fun getServiceConfig(): AuthorizationServiceConfiguration {
// Issuer URI (login URL in this case) dummy arg
return authServiceConfig = AuthorizationServiceConfiguration(
Uri.parse(issuerURI)
.buildUpon()
.appendEncodedPath("connect/authorize")
.build(),
Uri.parse(issuerURI)
.buildUpon()
.appendEncodedPath("connect/token")
.build()
)
}
private fun getTokenRequest(response: AuthorizationResponse) : TokenRequest {
val request = getAuthRequest()
val secret = RemoteConfig().clientSecret()
return TokenRequest.Builder(
request.configuration,
request.clientId)
.setGrantType(GrantTypeValues.AUTHORIZATION_CODE)
.setRedirectUri(request.redirectUri)
.setScope(request.scope)
// this is not valid in ID server
// .setCodeVerifier(request.codeVerifier)
.setAuthorizationCode(response.authorizationCode)
.setAdditionalParameters(mapOf("client_secret" to secret))
.build()
}
private fun performTokenRequest(authService: AuthorizationService,
req: TokenRequest,
appAuthState: AuthState,
resultCallBack:(result: AuthState?) -> Unit) {
authService
.performTokenRequest(req) { response, error ->
// Updates auth state based on if there's token response
// data or not.
if (response != null) {
appAuthState.update(response, error)
resultCallBack.invoke(appAuthState)
} else {
resultCallBack.invoke(null)
}
}
}
}
...但是@LargeTest
@RunWith(AndroidJUnit4::class)
class LoginAuthInstrumentedTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
@Rule
@JvmField
var activityTestRule = ActivityTestRule(LoginActivity::class.java)
@Test
fun loginAuthFlow_isCorrect() {
// Performs a click action in the login screen to fire off
// the auth service intent for an activity result.
onView(withId(R.id.start_auth_button)).perform(click())
// Automatically logs the user in with dummy creds within a
// custom Chrome tab intent (via the OpenID auth library).
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val selector = UiSelector()
val usernameInputObject = device.findObject(selector.resourceId("username"))
usernameInputObject.click()
usernameInputObject.text = "testuser@testapp.com"
val passwordInputObject = device.findObject(selector.resourceId("password"))
passwordInputObject.click()
passwordInputObject.text = "testpassword"
val loginBtnObject = device.findObject(selector.resourceId("cmdLogin"))
loginBtnObject.click()
// Upon a successful login from the auth service, the following
// asserts that the following views are shown on the next
// transitioned screen.
onView(withId(R.id.main_screen_header)).check(matches(withText(context.getString(R.string.main_screen_header_text))))
onView(withId(R.id.main_screen_subheader)).check(matches(withText(context.getString(R.string.main_screen_subheader_text))))
onView(withId(R.id.main_screen_description)).check(matches(withText(context.getString(R.string.main_screen_description_text))))
}
}
不能恢复,如日志中所示(LoginActivity
之前):
NoActivityResumedException