Hun's Blog

[Android] Android Test 2 - Unit Test (JUnit) 본문

Android

[Android] Android Test 2 - Unit Test (JUnit)

jhk-im 2020. 3. 22. 08:55

JUnit


-Java 프로그래밍 언어의 Unit Test 프레임워크이다.

JUnit을 사용해보기 전에 테스트에 대해서 조금 더 알아보자.

// 수동테스트 & 자동테스트
1. 수동테스트
-> 시간이 많이 걸림
-> 인건비 증가
-> 사람의 손으로 직접 하기 때문에 신뢰성 떨어짐
-> 프로그래밍 불가능

2. 자동테스트
-> 인적 자원보다 빠르게 테스트 가능
-> 인건비 감소
-> 사람의 손보다 안정적
-> 프로그래밍 가능

// Unit test & UI test
1. Local Unit Test (Unit Test)
-> JVM에서 실행된다.
-> 프로젝트 구조 : src/test/java

2. Instrumentation Test (UI Test)
-> 안드로이드 기기가 필요하다.
-> 프로젝트구조 : src/androidTest/java

즉, JUnit은 자동테스트를 구현할 수 있고 JVM 에서 실행되는 Local Unit Test 라는 것을 알 수 있다. 다음은 JUnit에서 지원하는 기본적인 메소드이다. 

- void assertEquals (expected,actual) - primitive / object 가 동일한 지 확인
- void assertTrue (condition) - 조건이 참인지 확인
- void assertFalse (object) - 객체가 null이 아닌지 확인한다.
- void assertNotSame (unexpected, actual) - 두개의 객체 참조가 동일한 객체를 가리키지 않는지
- void assertArrayEquals (expectedArray,actualArray) - 두 배열이 서로 같은지 테스트

 

JUnitExample / JUnitExampleTest 예제

 

JUnitExample을 생성하고 JUnitExampleTest에서 기본적인 JUnit의 메소드들을 사용해보자. 

 

app - build.gradle

1
2
3
// JUnit framework
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
 

안드로이드 스튜디오에서 프로젝트를 생성하면 기본적으로 JUnit framework가 추가되어있다.

 

TEST 해보기 위한 Order Coffee 응용프로그램 

1. res/values/strings.xml

1
2
3
4
5
6
7
8
<resources>
    <string name="app_name">JUnitSample</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>
 
 

 

2. CoffeeOrder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CoffeeOrder {
    private float mCoffeePrice;
    private int mCoffeeCount;
    private float mTotalPrice;
    public CoffeeOrder(float coffeePrice) {
        mCoffeeCount = 0;
        mTotalPrice = 0;
        this.mCoffeePrice = coffeePrice;
    }
    public void setCoffeeCount(int count) {
        if (count >= 0) {
            this.mCoffeeCount = count;
        }
        calculateTotalPrice();
    }
    public int getCoffeeCount() {
        return mCoffeeCount;
    }
    public void incrementCoffeeCount() {
        mCoffeeCount++;
        calculateTotalPrice();
    }
    public float getTotalPrice() {
        return mTotalPrice;
    }
    public void decrementCoffeeCount() {
        if (mCoffeeCount > 0) {
            mCoffeeCount--;
            calculateTotalPrice();
        }
    }
    private void calculateTotalPrice() {
        mTotalPrice = mCoffeePrice * mCoffeeCount;
    }
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none;color:white">cs

 

3. activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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>
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none;color:white">cs

 

4. MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private final static String COFFEE_COUNT = "coffee_count";
    public final static float DEFAULT_COFFEE_PRICE = 5.0f;
    private TextView mCoffeePrice;
    private TextView mTotalPrice;
    private TextView mCoffeeCount;
    private CoffeeOrder mOrder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCoffeePrice = (TextView) findViewById(R.id.coffee_price);
        mTotalPrice = (TextView) findViewById(R.id.total_price);
        mCoffeeCount = (TextView) findViewById(R.id.coffee_count);
        mCoffeePrice.setText(String.format(getString(R.string.coffee_price), DEFAULT_COFFEE_PRICE));
        mTotalPrice.setText(String.format(getString(R.string.total_price), 0.0f));
        findViewById(R.id.coffee_increment).setOnClickListener(this);
        findViewById(R.id.coffee_decrement).setOnClickListener(this);
        mOrder = new CoffeeOrder(DEFAULT_COFFEE_PRICE);
    }
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
    }
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        if (savedInstanceState != null) {
            mOrder.setCoffeeCount(savedInstanceState.getInt(COFFEE_COUNT));
            updateCoffeeCount();
            updateTotalPrice();
        }
    }
    private void updateCoffeeCount() {
        mCoffeeCount.setText(String.valueOf(mOrder.getCoffeeCount()));
    }
    private void updateTotalPrice() {
        mTotalPrice.setText(String.format(getString(R.string.total_price), mOrder.getTotalPrice()));
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.coffee_increment:
                mOrder.incrementCoffeeCount();
                updateCoffeeCount();
                updateTotalPrice();
                break;
            case R.id.coffee_decrement:
                mOrder.decrementCoffeeCount();
                updateCoffeeCount();
                updateTotalPrice();
                break;
        }
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none;color:white">cs

 

 

 

 

 

 

Order Coffee Test예제 

 

JUnit을 활용하여 위에서 만든 프로그램이 포함하고 있는 기능을 테스트해보자.

1. 커피 주문 객체가 null인가?
2. 커피 수량 체크
3. 커피 값 증가
4. 커피 값 감소
5. 총 가격 계산

그 전에 ExampleUnitTest에서 기본적으로 제공하는 메소드를 보면 @Test 라는 주석이 달려있는 것을 볼 수 있다. 이 주석에 대해서 먼저 알아보도록 하자. 

 

1
2
3
 @Test
public void addition_isCorrect() {
    assertEquals(42 + 2);}
 


@Test 

해당 주석이 있는 메소드는 테스트로 실행 될 수 있음음을 JUnit에 알려준다.

@Before
여러 테스트를 실행하기 전에 테스트할 객체를 만들때 사용한다.

@After
메소드에서 일부 외부 자원을 할당 할 경우 테스트 실행 후 해제해야한다.

@BeforeClass
public static void 메소드에 주석을 달면 테스트 메소드 전에 한번만 실행할 수 있다.

@AfterClass
모든 테스트가 완료된 후 메소드를 수행한다.

@Ignore
테스트를 무시하는데 사용된다. (해당 테스트는 실행되지 않는다.)


CoffeeOrderTest 
ExampleUnitTest가 있는 패키지에 생성한다.



0. Test 객체 셋팅 

1
2
3
4
@Before
public void setUp() {
    mOrder = new CoffeeOrder(PRICE_TEST);
}
 
 

셋팅이 되지 않으면 테스트를 할 수없다.


1. Test 객체 null 체크

1
2
3
4
@Test
public void orderIsNotNull() {
    assertNotNull(mOrder);
}
 

객체가 null 이면 알려준다.


2. 커피수량 체크

1
2
3
4
5
6
7
@Test
public void orderSetCoffeeCount() {
    mOrder.setCoffeeCount(-1);
    assertEquals(0, mOrder.getCoffeeCount());
    mOrder.setCoffeeCount(25);
    assertEquals(25, mOrder.getCoffeeCount());
}
 
 

-> 먼저 테스트객체의 메소드로 커피의 수량 -1로 셋팅하고 JUnit의 assertEqauls로 확인한다.
-> setCoffeCount는 0이거나 0보다 클 때 값이 셋팅되기 때문에 -1로 입력하면 값이 셋팅되지 않는다.
-> 이번에는 수량을 25로 셋팅하고 assertEqauls로 25가 입력되었는지 확인한다.


3. 커피 가격 증가

1
2
3
4
5
6
7
8
@Test
public void orderIncrement() {
    mOrder.incrementCoffeeCount();
    assertEquals(1, mOrder.getCoffeeCount());
    mOrder.setCoffeeCount(25);
    mOrder.incrementCoffeeCount();
    assertEquals(26, mOrder.getCoffeeCount());
}
 

-> incremetCoffeCount()는 커피 수량을 1씩 증가시킨다.
-> assertEqauls로 1이 되었는지 확인
-> 이번에는 수량을 25로 셋팅하고 incremetCoffeCount()로 1증가시킨다.
-> assertEqauls로 26이 되었는지 확인


4. 커피 가격 감소 

1
2
3
4
5
6
7
8
@Test
public void orderDecrement() {
    mOrder.decrementCoffeeCount();
    assertEquals(0, mOrder.getCoffeeCount());
    mOrder.setCoffeeCount(25);
    mOrder.decrementCoffeeCount();
    assertEquals(24, mOrder.getCoffeeCount());
}
 
 

-> decrementCoffeCount()는 커피 수량을 1씩 감소시킨다.
-> 커피 수량을 0보다 클때만 동작하기 때문에 감소되지 않았다.
-> 25로 셋팅후 1 감소시킨다음 24가 되었는지 확인한다.


5. 총 금액 

1
2
3
4
5
6
@Test
public void orderTotalPrice() {
    assertEquals(0.0f, mOrder.getTotalPrice());
    mOrder.setCoffeeCount(25);
    assertEquals(PRICE_TEST * 25, mOrder.getTotalPrice());
}
 
 

-> assertEquals로 총 금액을 확인한다.
-> setCoffeCount 안에는 총 금액을 계산하는 CacluateTotalPrice() 가 실행된다.
-> getTotalPrice() 는 총액을 반환한다.
-> 계산이 잘 되었는지 확인한다.


CoffeeOrderTest 전체코드


public class CoffeeOrderTest { private final static float PRICE_TEST = 5.0f; private CoffeeOrder mOrder; @Before public void setUp() { mOrder = new CoffeeOrder(PRICE_TEST); } @Test public void orderIsNotNull() { assertNotNull(mOrder); } @Test public void orderDecrement() { mOrder.decrementCoffeeCount(); assertEquals(0, mOrder.getCoffeeCount()); mOrder.setCoffeeCount(25); mOrder.decrementCoffeeCount(); assertEquals(24, mOrder.getCoffeeCount()); } @Test public void orderIncrement() { mOrder.incrementCoffeeCount(); assertEquals(1, mOrder.getCoffeeCount()); mOrder.setCoffeeCount(25); mOrder.incrementCoffeeCount(); assertEquals(26, mOrder.getCoffeeCount()); } @Test public void orderTotalPrice() { assertEquals(0.0f, mOrder.getTotalPrice()); mOrder.setCoffeeCount(25); assertEquals(PRICE_TEST * 25, mOrder.getTotalPrice()); } @Test public void orderSetCoffeeCount() { mOrder.setCoffeeCount(-1); assertEquals(0, mOrder.getCoffeeCount()); mOrder.setCoffeeCount(25); assertEquals(25, mOrder.getCoffeeCount()); } }

메소드 단위로 테스트

 

 

클래스 단위로 테스트 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class CoffeeOrderTest {
    private final static float PRICE_TEST = 5.0f;
    private CoffeeOrder mOrder;
    @Before
    public void setUp() {
        mOrder = new CoffeeOrder(PRICE_TEST);
    }
    @Test
    public void orderIsNotNull() {
        assertNotNull(mOrder);
    }
    @Test
    public void orderDecrement() {
        mOrder.decrementCoffeeCount();
        assertEquals(0, mOrder.getCoffeeCount());
        mOrder.setCoffeeCount(25);
        mOrder.decrementCoffeeCount();
        assertEquals(24, mOrder.getCoffeeCount());
    }
    @Test
    public void orderIncrement() {
        mOrder.incrementCoffeeCount();
        assertEquals(1, mOrder.getCoffeeCount());
        mOrder.setCoffeeCount(25);
        mOrder.incrementCoffeeCount();
        assertEquals(26, mOrder.getCoffeeCount());
    }
    @Test
    public void orderTotalPrice() {
        assertEquals(0.0f, mOrder.getTotalPrice());
        mOrder.setCoffeeCount(25);
        assertEquals(PRICE_TEST * 25, mOrder.getTotalPrice());
    }
    @Test
    public void orderSetCoffeeCount() {
        mOrder.setCoffeeCount(-1);
        assertEquals(0, mOrder.getCoffeeCount());
        mOrder.setCoffeeCount(25);
        assertEquals(25, mOrder.getCoffeeCount());
    }
}
 
 

 




정리 - 
JUnit을 활용해서 작은 응용프로그램의 기능들을 테스트해보았다. 
일반적으로 객체를 만들고 해당 객체의 멤버 변수들을 활용하여 액티비티 등에서 메소드를 활용해 값을 변경하며 프로그램을 동작시키게 된다. 어떠한 기능이 완성되고 당연히 해당 기능을 테스트해 보게 되는데 안드로이드애 앱을 빌드하여 직접 앱을 실행시켜 테스트해보는 것도 좋은 테스트 방법일 수 있다. 하지만 프로그램이 커지고 빌드시간이 늘어난다면? 혹은 방금 만든 기능이 액티비티를 타고 타고 들어가 버튼의 버튼을 눌러 팝업에서 동작하는 기능이라면? 그로인해 버려지는 시간이 상당 할 것이다. 

JUnit을 활용하면 해당객체를 Test하는 클래스를 생성하고 클래스 내부에 테스트하고자하는 객체를 생성해서 JUnit에서 제공하는 유용한 메소드를 활용해 간단하고 빠르게 테스트해 볼 수 있다.  테스트 속도로 치면 정말 극적으로 시간을 절약할 수 있다는 점이 인상깊다. 

무엇보다도 직접 프로그램을 만들 때 활용해 보는것이 가장 중요한 것 같다. 

 

 

참고 

https://alexzh.com/android-testing-unit-testing/

 

Android testing: Unit testing (Part 1) - Android development and testing

Today I would like the series of articles about Android Testing. I’m planning create articles about different type of…

alexzh.com