啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。

0. 简介 Service

Service 不一定用得很长久,那不就成了长佣了吗?我们可以用 JobIntentService ——临时佣人,它跟你的 App 同生共死,真好!但是,启动容易,关闭就毫无头绪了。因为 Service 在后台跑,跟 UI 是不沾边的。如果用 MVVM,我们可以塞 LiveData 。通过方程启动,系统会弹出 “LiveData has not initialed”。如果用 Service 的构造函数,系统会说不接受参数。饶头啊,对不?

没关系,我们可以使用插入式,我提议的是 Dagger-Hilt ,给系统打针。


1. MVVM 包

Gradle —— 资料库选择:

  • View Binding:
buildFeatures {
     
    viewBinding true
}
  • Dagger Hilt ——请自学安装。
  • ViewModel:
//region activity and fragment
// Activity and Fragment
def activity_version = "1.2.1"
implementation "androidx.activity:activity-ktx:$activity_version"
def fragment_version = "1.3.2"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
//endregion

这里随便提提,其实你们可以抄我以前写的,Gradle 太占地方了,所以省略一二。

MVVM —— 文件排列

啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第1张图片

Helper —— 帮手

  • helper/LogHelper.kt
import android.util.Log

const val TAG = "MLOG"
fun lgd(s:String) = Log.d(TAG, s)
fun lgi(s:String) = Log.i(TAG, s)
fun lge(s:String) = Log.e(TAG, s)
fun lgv(s:String) = Log.v(TAG, s)
fun lgw(s:String) = Log.w(TAG, s)
  • helper/MessageHelper.kt
import android.content.Context
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import android.widget.Toast.LENGTH_SHORT

fun msg(context: Context, s: String, len: Int) =
    if (len > 0) Toast.makeText(context, s, LENGTH_LONG).show()
    else Toast.makeText(context, s, LENGTH_SHORT).show()

️ 2. UI Design 平面设计

啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第2张图片
et_message :输入资料进Service。
tv_service :Service 的反应。


3. JobIntentService

JobIntentService 是 IntentService 的改良版。

开始服务

这个服务是用方程启动的——enqueueWork

fun enqueueWork(context: Context, work: Intent) {
     
    enqueueWork(context, MyIntentService::class.java, JOB_ID, work)
}

这个 enqueueWork 有 4 种参数:

  1. Context
  2. Service class
  3. Job ID
  4. Intent

⌛️ 关闭服务

用 instance 关闭。

class MyIntentService: JobIntentService() {
     

    init {
     
        instance = this
    }
    
	companion object {
     
		private lateinit var instance: MyIntentService
		private val JOB_ID = 4343443

		fun enqueueWork(context: Context, work: Intent) {
     ...}

		fun stopService() {
     
			lgd("MyIntentService: Service is stopping...")
			instance.stopSelf()
		}
	}
}

你瞧,自己关自己。


4. Permission

AndroidManifest.xml

<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
    ...
     android:name=".ui.MainActivity">
        ...
    activity>
    <service android:name=".service.MyIntentService"
        android:permission="android.permission.BIND_JOB_SERVICE"
        android:exported="true" />
application>

✒️ ui/MainActivity.kt

// check manifests for permissions
private val REQUIRED_PERMISSIONS = arrayOf(
    Manifest.permission.WAKE_LOCK
)

class MainActivity : AppCompatActivity() {
     

    // app permission
    private val reqMultiplePermissions = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {
      permissions ->
        permissions.entries.forEach {
     
            lgd("mainAct: Permission: ${
       it.key} = ${
       it.value}")
            if (!it.value) {
     
                // toast
                msg(this, "Permission: ${
       it.key} denied!", 1)
                finish()
            }
        }
    }

    // =============== Variables
    // view binding
    private lateinit var binding: ActivityMainBinding
    // view model
    val viewModel: MainViewModel by viewModels()

    // =============== END of Variables

    override fun onCreate(savedInstanceState: Bundle?) {
     
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // check app permissions
        reqMultiplePermissions.launch(REQUIRED_PERMISSIONS)
    }

    companion object {
     
        const val USER_INPUT = "USER_INPUT"
    }
}

⌚5. Observables & Hilt 观察和打针

‍ 观察点

我需要提供两个观察点:

  1. isRunning:服务状态。
  2. userInput:客户输入的内容。有两种方式 IntentExtra 和 LiveData 。我将会测试那种有保证。

app/ServiceApp.kt 提供 Hilt 应用

@HiltAndroidApp
class ServiceApp: Application()

di/LiveDataModule.kt

@Module
@InstallIn(SingletonComponent::class)
object LiveDataModule {
     

    @Provides
    @Singleton
    fun provideServiceStatus(): 
        MutableLiveData<Boolean> = MutableLiveData<Boolean>()

    @Provides
    @Singleton
    fun provideUserInput(): 
        MutableLiveData<String> = MutableLiveData<String>()
    
}

简单吧,就伺候这俩。

♈️ ui/MainViewModel.kt

@HiltViewModel
class MainViewModel @Inject constructor(
    val isRunning: MutableLiveData<Boolean>,
    private val userInput: MutableLiveData<String>
): ViewModel() {
     

    init {
     
        isRunning.value = false
        userInput.value = ""
    }

    fun enableService() {
     
        isRunning.postValue(true)
    }

    fun updateUserInput(inputText: String) {
     
        userInput.postValue(inputText)
    }
}

主要功能是更新画面。

service/MyIntentService.kt

@AndroidEntryPoint
class MyIntentService: JobIntentService() {
     

    @Inject
    lateinit var isRunning: MutableLiveData<Boolean>

    @Inject
    lateinit var userInput: MutableLiveData<String>

    init {
     
        instance = this
    }

    override fun onHandleWork(intent: Intent) {
     
        lgd("onHandleWork")
        try {
     
            lgd("MyIntentService: Service is running...")
            if (isRunning.value!!) {
     

                // check Intent Extra
                val extraInput = intent.getStringExtra(USER_INPUT)
                lgd("Intent Extra: $extraInput")
                
                var input = "Empty"
                if (userInput.value != "")
                    input = userInput.value.toString()

                lgd("receive text from LiveData: $input")

                for (i in 0..9) {
     
                    lgd("Input: $input - $i")
                    if (isRunning.value == false)
                        return
                    SystemClock.sleep(1000)
                }
                stopService()
            }
        } catch (e: InterruptedException) {
     
            Thread.currentThread().interrupt()
        }
    }
...

ui/MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
     
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
     
        ...

        binding.btStart.setOnClickListener {
     
            viewModel.enableService()

            // update user input
            val inputText = binding.etMessage.text
            if (!inputText.isEmpty() || !inputText.isBlank())
                viewModel.updateUserInput(inputText.toString())
            lgd("input text: $inputText")

            val mIntent = Intent(this, MyIntentService::class.java)
            mIntent.putExtra(USER_INPUT, inputText)

            // start service
            MyIntentService.enqueueWork(this, mIntent)
        }

        binding.btStop.setOnClickListener {
     
            MyIntentService.stopService()

        }

        // observer
        viewModel.isRunning.observe(this, {
     
            if (it)
                binding.tvService.text = "Service is Start..."
            else
                binding.tvService.text = "Service is Stop!"
        })
    }

    companion object {
     ...}
}

按键提供打开服务,关闭服务。
还有就是观察服务状态。


6. 运行结果

啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第3张图片
状态良好。看看 Logcat:

--------- beginning of system
2021-04-24 14:39:27.113 6199-7154 D/MLOG: onHandleWork
2021-04-24 14:39:27.113 6199-7154 D/MLOG: MyIntentService: Service is running...
2021-04-24 14:39:27.123 6199-7154 D/MLOG: Intent Extra: null
2021-04-24 14:39:27.123 6199-7154 D/MLOG: receive text from LiveData: Empty
2021-04-24 14:39:27.123 6199-7154 D/MLOG: Input: Empty - 0
2021-04-24 14:39:28.164 6199-7154 D/MLOG: Input: Empty - 1
2021-04-24 14:39:29.205 6199-7154 D/MLOG: Input: Empty - 2
2021-04-24 14:39:30.246 6199-7154 D/MLOG: Input: Empty - 3
2021-04-24 14:39:31.273 6199-7154 D/MLOG: Input: Empty - 4
2021-04-24 14:39:32.318 6199-7154 D/MLOG: Input: Empty - 5
2021-04-24 14:39:33.360 6199-7154 D/MLOG: Input: Empty - 6
2021-04-24 14:39:34.375 6199-7154 D/MLOG: Input: Empty - 7
2021-04-24 14:39:35.380 6199-7154 D/MLOG: Input: Empty - 8
2021-04-24 14:39:36.384 6199-7154 D/MLOG: Input: Empty - 9
2021-04-24 14:39:37.387 6199-7154 D/MLOG: MyIntentService: Service is stopping...

加入 Welcome, 看看 Service 是否收到。
啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第4张图片
Logcat:

2021-04-24 14:44:55.110 6199-6199 D/MLOG: input text: Welcome
2021-04-24 14:44:55.126 6199-7154 D/MLOG: onHandleWork
2021-04-24 14:44:55.126 6199-7154 D/MLOG: MyIntentService: Service is running...
2021-04-24 14:44:55.127 6199-7154 D/MLOG: Intent Extra: null
2021-04-24 14:44:55.127 6199-7154 D/MLOG: receive text from LiveData: Welcome
2021-04-24 14:44:55.127 6199-7154 D/MLOG: Input: Welcome - 0
2021-04-24 14:44:56.168 6199-7154 D/MLOG: Input: Welcome - 1
2021-04-24 14:44:57.211 6199-7154 D/MLOG: Input: Welcome - 2
2021-04-24 14:44:58.251 6199-7154 D/MLOG: Input: Welcome - 3
2021-04-24 14:44:59.293 6199-7154 D/MLOG: Input: Welcome - 4
2021-04-24 14:45:00.334 6199-7154 D/MLOG: Input: Welcome - 5
2021-04-24 14:45:01.377 6199-7154 D/MLOG: Input: Welcome - 6
2021-04-24 14:45:02.418 6199-7154 D/MLOG: Input: Welcome - 7
2021-04-24 14:45:03.460 6199-7154 D/MLOG: Input: Welcome - 8
2021-04-24 14:45:04.501 6199-7154 D/MLOG: Input: Welcome - 9
2021-04-24 14:45:05.542 6199-7154 D/MLOG: MyIntentService: Service is stopping...

IntentExtra 接收失败。
LiveData 成功。


☕ 7. Espresso 测试

➕ 加测试

右键点击 “class MyIntentService”, 然后 Alt+Insert 。
啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第5张图片
选 Junit4:
啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第6张图片
androidTest 文件夹:
啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第7张图片

service/MyIntentServiceTest.kt

@ExperimentalCoroutinesApi
@LargeTest
@HiltAndroidTest
class MyIntentServiceTest {
     

    @get:Rule(order = 1)
    var hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 2)
    var activityRule = ActivityScenarioRule(MainActivity::class.java)


    @Before
    fun setup() {
     
        hiltRule.inject()
    }

helper/Constants.kt —— 常数

const val START = "Service is Start..."
const val STOP = "Service is Stop!"

❌ Stop Test Case —— 停止测试

@Test
fun test_stop_service_espresso() {
     
    lgd("=====>  Stop Service Test")

    // start activity
    val scenario = activityRule.getScenario()

    onView(withId(R.id.bt_start)).perform(click())
    lgd("=====>  Start Button Clicked")

    onView(withId(R.id.bt_stop)).perform(click())
    lgd("=====>  Stop Button Clicked")

    val serviceMsg = onView(withId(R.id.tv_service))
    serviceMsg.check(ViewAssertions.matches(
        ViewMatchers.withText(STOP)))
}

啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第8张图片
好像很正常,让我们看看 Logcat :

2021-04-27 07:57:31.223 20493-20703 D/MLOG: MyIntentService: Service is running...
2021-04-27 07:57:31.230 20493-20703 D/MLOG: Intent Extra: null
2021-04-27 07:57:31.230 20493-20703 D/MLOG: receive text from LiveData: Empty
2021-04-27 07:57:31.230 20493-20703 D/MLOG: Input: Empty - 0
2021-04-27 07:57:32.278 20493-20703 D/MLOG: Input: Empty - 1
2021-04-27 07:57:33.313 20493-20703 D/MLOG: Input: Empty - 2
2021-04-27 07:57:34.345 20493-20703 D/MLOG: Input: Empty - 3
2021-04-27 07:57:35.356 20493-20703 D/MLOG: Input: Empty - 4
2021-04-27 07:57:36.393 20493-20703 D/MLOG: Input: Empty - 5
2021-04-27 07:57:37.434 20493-20703 D/MLOG: Input: Empty - 6
2021-04-27 07:57:38.473 20493-20703 D/MLOG: Input: Empty - 7
2021-04-27 07:57:39.475 20493-20703 D/MLOG: Input: Empty - 8
2021-04-27 07:57:40.521 20493-20703 D/MLOG: Input: Empty - 9
2021-04-27 07:57:41.558 20493-20703 D/MLOG: MyIntentService: Service is stopping...
2021-04-27 07:57:41.792 20493-20533 D/MLOG: =====>  Start Button Clicked
2021-04-27 07:57:41.850 20493-20493 D/MLOG: MainAct: stop button clicked!
2021-04-27 07:57:41.850 20493-20493 D/MLOG: MyIntentService: Service is stopping...
2021-04-27 07:57:42.089 20493-20533 D/MLOG: =====>  Stop Button Clicked

糟透了,Espresso 在 Service 停止后才出来干活。因此,我们需要另一种工具来测试 —— UiAutomator 。


8. UiAutomator

gradle.module

//UiAutomator
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'

Sync。

MyIntentJobServiceTest

@get:Rule(order = 1)
var hiltRule = HiltAndroidRule(this)

这个保留。

private var mDevice: UiDevice? = null

启动:

@Before
fun setup() {
     
    hiltRule.inject()

    // Initialize UiDevice instance
    mDevice = UiDevice.getInstance(getInstrumentation())
    mDevice!!.pressMenu()
    val launcherPackage = mDevice!!.launcherPackageName
    Truth.assertThat(launcherPackage).isNotNull()

    mDevice!!.wait(
        Until.hasObject(By.pkg(launcherPackage).depth(0)),
        LAUNCH_TIMEOUT
    )

    // launch app
    val context = ApplicationProvider.getApplicationContext<Context>()
    val intent = context.packageManager.getLaunchIntentForPackage(
        APPLICATION_ID)?.apply {
     
        // Clear out any previous instances
        addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
    }
    context.startActivity(intent)

    // Wait for the app to appear
    mDevice!!.wait(
        Until.hasObject(By.pkg(APPLICATION_ID).depth(0)),
        LAUNCH_TIMEOUT
    )
}
companion object {
     
    const val LAUNCH_TIMEOUT = 5000L
}

加 Test :

@Test
fun test_stop_service_uiautomator() {
     
}

用 ID 找物体

UiAutomator 找东西跟安卓是一样的:

val $var$ = mDevice!!.findObject(
    res("${
       APPLICATION_ID}:id/$obj$"))

这是 Live Template 捷径。

让我们加两个按键:

// buttons
val startBt = mDevice!!.findObject(
    res("${
       APPLICATION_ID}:id/bt_start"))
val stopBt = mDevice!!.findObject(
    res("${
       APPLICATION_ID}:id/bt_stop"))

同样过程再试试:启动服务 → 停止服务 → 检查

lgd("start bt: ${
       startBt.resourceName}")
startBt.click()

Thread.sleep(1000L)

lgd("stop bt: ${
       stopBt.resourceName}")
stopBt.click()

// service message
val serviceMsg = mDevice!!.findObject(
    res("${
       APPLICATION_ID}:id/tv_service"))
val statusStr = serviceMsg.text
Truth.assertThat(statusStr).isEqualTo(STOP)

跑啊:
啪!啪!给 JobIntentService 打针, Hilt 号的大针,看你爽不爽?哎呦,Espresso 看不到结果,用 UiAutomator 测。_第9张图片
这次,你应该看到测试没有用到 10 秒,几乎立刻就停止了。所以这是成功的测试。


9. UiAutomator — UserInput 输入测试

‍ LogOutput.kt —— Log 输出

我们可以截取一部分 Log 来检查成果。

/**
 *  Editor: Homan Huang
 *  Date: 04/27/2021
 */
/**
 *  Get Logcat output by getOutput("logcat *:S TAG -d")
 */
fun getOutput(command: String): String? {
     
    val proc = Runtime.getRuntime().exec(command)

    try {
     
        val stdInput = BufferedReader(
            InputStreamReader(
                proc.inputStream
            )
        )
        val output = StringBuilder()
        var line: String? = ""
        //var counter = 0
        while (stdInput.readLine().also {
      line = it } != null) {
     
            //counter += 1
            //lgd("line #$counter = $line")
            output.append(line+"\n")
        }
        stdInput.close()
        return output.toString()
    } catch (e: IOException) {
     
        e.printStackTrace()
    }
    return null
}

/**
 *  clear logcat buffer
 */
fun clearLog() {
     
    Runtime.getRuntime().exec("logcat -c")
}

输入测试

/**
 *  Test: Input the message;
 *        start the service;
 *        and check logcat
 */
@Test
fun message_input_service_uiautomator() {
     
    // buttons
    val msgInput = mDevice!!.findObject(
        res("${
       APPLICATION_ID}:id/et_message"))
    val startBt = mDevice!!.findObject(
        res("${
       APPLICATION_ID}:id/bt_start"))
    val stopBt = mDevice!!.findObject(
        res("${
       APPLICATION_ID}:id/bt_stop"))

    // clear Logcat buffer
    clearLog()

    // input
    val toServiceStr = "This is a test."
    msgInput.text = toServiceStr
    Thread.sleep(1000)

    lgd("start bt: ${
       startBt.resourceName}")
    startBt.click()

    Thread.sleep(1000)

    lgd("stop bt: ${
       stopBt.resourceName}")
    stopBt.click()

    val param = "logcat *:S MLOG -d"
    lgd("param: $param")
    val mLog = getOutput(param)

    Thread.sleep(500)

    lgd("mlog: $mLog")

    Truth.assertThat(mLog?.contains("Input: $toServiceStr"))
        .isTrue()
}

跑啊!截取 Logcat:

mlog: --------- beginning of main
    04-27 14:06:29.582 31368 31368 D MLOG    : mainAct: Permission: android.permission.WAKE_LOCK = true
    04-27 14:06:31.459 31368 31400 D MLOG    : start bt: com.homan.huang.servicedemo:id/bt_start
    04-27 14:06:31.504 31368 31368 D MLOG    : MainAct: start button clicked!
    04-27 14:06:31.504 31368 31368 D MLOG    : input text: This is a test.
    04-27 14:06:31.527 31368 31419 D MLOG    : onHandleWork
    04-27 14:06:31.527 31368 31419 D MLOG    : MyIntentService: Service is running...
    04-27 14:06:31.528 31368 31419 D MLOG    : Intent Extra: null
    04-27 14:06:31.528 31368 31419 D MLOG    : receive text from LiveData: This is a test.
    04-27 14:06:31.528 31368 31419 D MLOG    : Input: This is a test. - 0
    04-27 14:06:32.495 31368 31400 D MLOG    : stop bt: com.homan.huang.servicedemo:id/bt_stop
    04-27 14:06:32.519 31368 31400 D MLOG    : param: logcat *:S MLOG -d
    04-27 14:06:32.525 31368 31368 D MLOG    : MainAct: stop button clicked!
    04-27 14:06:32.525 31368 31368 D MLOG    : MyIntentService: Service is stopping...

测试通过!


10. 英文版

帮帮忙,拍拍手!
英文连接: Inject LiveData into JobIntentService and How to Test It

你可能感兴趣的:(测试,android,程编,Kotlin,安卓,编程语言,测试类型,kotlin)