유닛테스트 코드를 작성한 이유
앱을 개발하다보면 코드가 예상대로 실행이 되는지 테스트가 필요합니다.
하지만 매번 수동으로 테스트하게 되면 인력 소모도 심하고, 수동으로 하다보니 매번 정확하지 않은 테스트가 수행될 수 있어 안정성이 떨어질 수 있습니다.
만약 자동화 테스트를 진행하게 된다면 인력 소모를 줄일 수 있고, 보다 정교하고 안정적인 테스트를 진행할 수 있다고 생각했습니다.
이 포스팅의 목적
이번 포스팅은 로컬 JVM 에서 실행할 수 있는 유닛 테스트 코드 작성입니다.
구글링한 자료를 바탕으로 Kotlin 언어로 작성해 보았습니다.
우선 위 이미지에서 테스트 환경 프로젝트 구조를 볼 수 있습니다. ( 샘플 예제 코드와는 관련이 없습니다. )
~androidTest/java/com.example.androidtest/ExampleInstrumentedTest는 실제 안드로이드 기기를 통한 테스트 방법이고,
~test/java/com.example.androidtest/ExampleUnitTest가 유닛테스트, 즉 이번 포스팅에서 다루게 될 로컬 JVM에서 실행할 수 있는 테스트 방법입니다.
build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.androidtest"
minSdkVersion 24
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
// android test implementation
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
values/strings
<resources>
<string name="app_name">AndroidTest</string>
<string name="coffee_price">Coffee: $%.1f</string>
<string name="total_price">Total price: $%.1f</string>
<string name="increment_label">+</string>
<string name="decrement_label">-</string>
<string name="default_coffee_count">0</string>
</resources>
acitivity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/coffee_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@string/coffee_price"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:gravity="center_vertical">
<Button
android:id="@+id/coffee_decrement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:text="@string/decrement_label"/>
<TextView
android:id="@+id/coffee_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textSize="32sp"
android:text="@string/default_coffee_count"/>
<Button
android:id="@+id/coffee_increment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:text="@string/increment_label"/>
</LinearLayout>
<TextView
android:id="@+id/total_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@string/total_price"/>
</LinearLayout>
MainActivity.class
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.PersistableBundle
import android.util.Log
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
val COFFEE_COUNT = "coffee_count"
val DEFAULT_COFFEE_PRICE = 5.0f
private lateinit var mOrder : CoffeeOrder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mOrder = CoffeeOrder(DEFAULT_COFFEE_PRICE)
coffee_price.text = String.format(getString(R.string.coffee_price),DEFAULT_COFFEE_PRICE)
total_price.text = String.format(getString(R.string.total_price),0.0f)
coffee_increment.setOnClickListener {
mOrder.incrementCoffeeCount()
updateCoffeeCount()
updateTotalPrice()
}
coffee_decrement.setOnClickListener {
mOrder.decrementCoffeeCount()
updateCoffeeCount()
updateTotalPrice()
}
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState!!.putInt(COFFEE_COUNT,mOrder.mCoffeeCount)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
if(savedInstanceState != null){
mOrder.mCoffeeCount = savedInstanceState.getInt(COFFEE_COUNT)
updateCoffeeCount()
updateTotalPrice()
}
}
private fun updateCoffeeCount(){
coffee_count.text = ""+mOrder.mCoffeeCount
}
private fun updateTotalPrice(){
total_price.text = String.format(getString(R.string.total_price),mOrder.mTotalPrice)
}
}
CoffeeOrder.class
class CoffeeOrder(coffeePrice : Float) {
var mCoffeePrice = coffeePrice
var mCoffeeCount = 0
get() = if(field < 0) 0 else field
var mTotalPrice = 0f
fun incrementCoffeeCount(){
mCoffeeCount++
calculateTotalPrice()
}
fun decrementCoffeeCount(){
if( mCoffeeCount > 0){
mCoffeeCount--
calculateTotalPrice()
}
}
fun calculateTotalPrice(){
mTotalPrice = mCoffeePrice * mCoffeeCount
}
}
CoffeeOrderTest.class
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
class CoffeeOrderTest {
private val PRICE_TEST = 5.0f
private lateinit var mOrder : CoffeeOrder
@Before
fun setUp(){
mOrder = CoffeeOrder(PRICE_TEST)
}
@Test
fun orderIsNotNull(){
assertNotNull(mOrder)
}
@Test
fun orderDecrement(){
mOrder.decrementCoffeeCount()
assertEquals(0, mOrder.mCoffeeCount)
mOrder.mCoffeeCount = 25
mOrder.decrementCoffeeCount()
assertEquals(24,mOrder.mCoffeeCount)
}
@Test
fun orderIncrement(){
mOrder.incrementCoffeeCount()
assertEquals(1,mOrder.mCoffeeCount)
mOrder.initCoffeeCount(25)
mOrder.incrementCoffeeCount()
assertEquals(26,mOrder.mCoffeeCount)
}
@Test
fun orderTotalPrice(){
assertEquals(0.0f, mOrder.mTotalPrice)
mOrder.initCoffeeCount(25)
assertEquals(PRICE_TEST*25, mOrder.mTotalPrice)
}
@Test
fun orderSetCoffeeCount(){
mOrder.initCoffeeCount(-1)
assertEquals(0, mOrder.mCoffeeCount)
mOrder.initCoffeeCount(25)
assertEquals(25, mOrder.mCoffeeCount)
}
}
실행
실행 결과
만약 기대값과 다른 결과가 나온다면?
위 이미지에서 볼 수 있듯이 코드 왼쪽에 빨간 아이콘이 보입니다.
( 해당 테스트코드가 실패했을 경우 빨간색. 성공했다면 초록색이 보일겁니다. )
각각의 @Test 어노테이션이 붙은 코드 블록에서 아이콘을 확인할 수 있습니다. 즉, 각 메소드마다 테스트를 해볼 수 있습니다.
위 이미지에서 기대값이 -1인데 실재 값이 0으로 나왔을 때 아래와 같은 에러 코드를 볼 수 있습니다.
이것저것 테스트해보고 있는데 유닛테스트를 더 자세하게 학습하고 싶어지네요!
읽어주셔서 감사합니다.
- 참고 자료
'Android > Android Testing ' 카테고리의 다른 글
Android Mockito를 사용한 유닛 테스트 코드 작성 (0) | 2019.07.12 |
---|---|
Android Espresso를 사용한 UI 테스트 예제 코드 작성 (0) | 2019.07.11 |
Android testing codelab ModuleComponentIdentifierImpl error (0) | 2019.07.10 |