봄날은 갔다. 이제 그 정신으로 공부하자

우선 UNIT Test부터 적용하기 본문

학습

우선 UNIT Test부터 적용하기

길재의 그 정신으로 공부하자 2020. 11. 25. 13:54

이 글은 현재 재직중인 회사의 앱에 UNIT 테스트를 적용한 후기 글 입니다.

이전 글을 읽지 않으신 분은 이전 글로...

als2019.tistory.com/12

 

테스트 자동화 어떻게 만들까?

Unit 테스트를 시작하기 전에… 해당 글은 Unit 테스트 기능을 개발 초기에 마주하는 문제에 대해 간단하게 기술합니다. 해당 글은 테스트 자동화 두번째 글이므로 이전 글을 읽지 않은

als2019.tistory.com

 

시작

무엇을 어떻게 테스트 테스트할건지 계획을 세우는게 시작 입니다.

 

앱은 서버와 주고 받은 데이터를 기준으로 동작합니다.

사용자가 앱을 실행하거나 결제할 데이터를 생성하거나 결제를 완료할 때 거의 모든 사용자 이벤트마다 앱은 서버에 데이터를 요청하고 데이터를 받아 가공한 후 사용자에게 제공합니다.

 

또한 앱은 가입한 사용자에 따라 서로 다른 정책과 기능을 지원하므로 서로 다른 다양한 정책에 맞게 앱과 서버가 정상적으로 정보 주고 받는지 확인해야 합니다.

 

UNIT 테스트할 부분 정하기

위 열거한 내용을 고려하여 UNIT 테스트 FUNCTION은 아래와 같이 정했습니다.

  • 인증 UNIT

  • 사용자 정보 UNIT

  • 결제 목록 UNIT

  • 결제하기 UNIT

  • 결제 취소하기 UNIT

  • 통합 결제 목록 UNIT

  • 통합 결제하기 UNIT

  • 통합 결제 취소하기 UNIT

 

아래 FUNCTION의 범위는 앱이 서버에 정보를 요청하고 서버로 부터 전달 받은 정보를 자료구조화 하는 부분까지로 예를 들어  서버로부터 전달받은 아래와 같은 JSON format data의 무결성을 체크하는 부분까지로 범위를 한정하였습니다.

 

Junit class 특이사항 되새김질

UNIT 테스트를 위해 android에서 지원하는 Junit class는 몇가지 특이사항이 있습니다.

class 내의 함수 호출이 랜덤하다는 제약과 class 내에 멤버 변수 유지가 불가하다는 제약 입니다.

이부분을 자세히 설명하면 아래와 같습니다.

 

class 내의 테스트 함수가 랜덤하게 호출됨.

제가 만들려는 것은 테스트 순서가 일정하고 순차적으로 호출되어야 하기 때문에 Junit class 상단에 아래와 같은 어노테이션을 추가해주어야 합니다.

@FixMethodOrder(MethodSorters.NAME_ASCENDING)

이와 같은 어노테이션을 추가하게 되면 function의 이름순으로 정렬되어 호출되므로 function 이름을 정할 때 규칙을 정해야 합니다.

TestA, TestB, ...

 

class 내에서 멤버 변수 유지가 불가함.

TestA 함수가 호출 될 때 10을 출력하고 TestB 함수가 호출될 때 20을 출력하는 것을 기대하고 아래 test class를 만드는 경우, 로그는 항상 10이 출력됩니다.

@Rule
public int count = 0;

@Before
public void initUnit() {
    count = 10;
}

@Test
public void TestA(){
    Log.d("LOG", "count: " + count);
    count  =20;
}

@Test
public void TestB(){
    Log.d("LOG", "count: " + count);
}

Junit class를 잘 모르면 아래와 같은 함수 호출 순서를 기대하지만

   initUnit() → TestA() → TestB()

 

실제로는 아래와 같은 순서로 호출됩니다.

   initUnit() → TestA() → initUnit() → TestB()

 

즉, 모든 @Test 어노테이션 호출 전에 반드시 @Before 어노테이션이 호출됩니다.

 

Context 처리

Junit 특이사항은 아니지만 앱 특성상 Network 처리 부분에서 Context를 사용하는 부분이 있습니다.

이러한 부분을 위해 @Test 함수 호출 전에 Activity가 초기화 될 수 있도록 아래와 같이 class를 만들어줍니다.

@Rule
public ActivityTestRule<ActLogin> activityRule = new ActivityTestRule(ActLogin.class);
private ActLogin activity;

@Before
public void initUnit() {
    activity = activityRule.getActivity();
}

여기서 유의할 부분은 생성해주는 activity를 코드상에서 임의로 삭제하거나 하면 안됩니다.

그리고 가급적이면 생성하는 activity는 실행 후 아무런 동작도 하지 않는 것으로 만들어주면 좋습니다.

실행 후 다른 activity로 자동으로 넘어가거나 값을 변경하거나 하면 오동작을 할 수 있기 때문 입니다.

 

이 부분은 UI Test(Espresso)와 관련된 부분으로 UI Test를 소개하는 글에서 좀 더 자세히 기술하도록 하겠습니다.

 

UI Thread 처리

테스트하려는 함수 중 대부분이 서버와 비동기 통신을 하다보니 AsyncTask로 개발되었고 이벤트를 받는 과정에서 UI Thread로의 처리가 필요합니다.

이부분 처리를 위해 아래와 같이 runOnUiThread를 임포트하고 

import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;

테스트 함수에 아래와 같이 runOnUiThread()를 사용해 줍니다.

@Test(timeout = 10000)
public void TestB(){
    try {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                new AuthScenario().run(activity, "00000000000", "0000");
            }
        });
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    zzzzz(7000);
}

 

테스트 구조 만들기

위와 같은 특이사항을 고려하여 기존 개발 아키텍쳐의 MVP or MVC or MCCM 구조와 매우 유사한 USM 구조를 만들어보았습니다.

USM 구조는 각 단위별 결합도를 낮추기 위해 Unit→ Scenario→ Model 순서로 단방향 구조를 가지고 있습니다.

Unit와 Scenario는 "1 대 1 관계"이며 Scenario는 Model과 "1 대 다 관계" 입니다.

 

Unit이 테스트하려는 Scenario를 선택하고 테스트 계정 정보(ID & password)를 입력하면 Scenario는 시나리오를 실행하는데 필요한 model 인스턴스를 한개 이상 생성하여 테스트를 수행합니다.

 

위 구조의 장점은 다양한 정책 추가 및 추가되는 테스트 시나리오의 대응이 빠르다는데 있습니다. (물론 제 기준입니다. ㅋㅋ)

서로 독립된 구조를 가지고 있으므로,

정책이 추가된다면 Unit class로 추가하고

테스트 시나리오가 변경 or 추가 된다면 Scenario class를 추가하고

테스트할 기능이 추가된다면 Model class를 추가하면 되는 구조 입니다.

 

개발 예시

예를 들어 앱에서 2개의 서로 다른 정책에 대해 결제 기능을 테스트 한다고 하면 아래와 같은 class들이 사용됩니다.

 

UnitPayment는 2개 정책에 따른 결제 테스트를 위해 두 개의 PaymentConfirmScenario 인스턴스를 생성하고

PaymentConfirmScenario는 시나리오 실행을 위해 AuthTest, UserInfoTest, PaymentTest 인스턴스를 생성합니다.

 

코드로 설명하면 아래와 같습니다. (원활한 설명을 위해 실제 코드를 간략화하였습니다.)

// UnitPayment class

@Test(timeout = 10000)
public void TestB(){
 new PaymentConfirmScenario().run(activity, "0000000001", "0001", PaymentTest.POLICY_1);
}

@Test(timeout = 10000)
public void TestC(){
    new PaymentConfirmScenario().run(activity, "0000000002", "0002", PaymentTest.POLICY_2);
}


// PaymentConfirmScenario class

public void run(final Activity activity, final String id, final String password, final int policy){
    AuthTest auth = new AuthTest(activity, id, password);
    UserInfoTest userInfo = new UserInfoTest(activity, id, password);
    PaymentTest payment = new PaymentTest(activity, id, password, policy);
 
    auth.login();
    userInfo.userInfos();
    payment.setStoreList();
    payment.setCashList();
    payment.paymentRequest();
    payment.paymentCheck();
    payment.paymentAccept();
    auth.logout(handler, FINISH);
}


// AuthTest class

public void login(){
    OkHttpModule network = new OkHttpModule(activity, false, new OkHttpModule.ResponseListener(){
        @Override
        public void success(Response response) {
            Assert.assertNotNull(response);
            JSONObject jsonObject = new JSONObject(response.body().string());
            Assert.assertNotNull(jsonObject);
            authKey = ParsingNetData.getString(jsonObject, "auth_key");
            Assert.assertNotNull(auth_key);
            userNo = ParsingNetData.getInt(jsonObject, "user_no");
            Assert.assertNotEquals(userNo, ERROR_INT);
            int err_code = ParsingNetData.getInt(jsonObject, "code");
            Assert.assertEquals(err_code, ERROR_INT);
        }
        
        @Override
        public void fail(IOException e) {
            Assert.fail();
        }
   });

   network.METHOD = "POST";
   network.getClient();
 }
 ...

'학습' 카테고리의 다른 글

MotionLayout - xml 구성요소  (0) 2020.11.25
MotionLayout - overview  (0) 2020.11.25
UI Test 적용하기  (0) 2020.11.25
테스트 자동화 어떻게 만들까?  (0) 2020.11.25
조금 더 개발에 집중하기 위한 테스트 자동화 검토  (0) 2020.11.25
Comments