[안드로이드스튜디오_문화][좋은 아키텍처를 위한 올바른 테스트]

기말 지하기포·2024년 1월 13일
0

아키텍처에서 테스트가 중요한 이유


  • 현실적인 필요성
    -아키텍처를 수정 할 때마다 많은 변화가 수반이 되고 앱에 굉장히 많은 영향이 갈 수 있다. 그렇기 때문에 그때마다 많은 테스트가 필요한데 다 사람들에게 맡긴다면 많은 시간과 비용이 소모된다.

  • 좋은 설계를 촉진
    -단일 책임의 원칙을 잘 지키지 않느 클래스라고 하면 여러개의 책임을 지니고 있는 것이고 , 개방 폐쇄 원칙을 잘 지키지 않는다면 필요없는 의존성을 내부에 갖고 있거나 하는 경우가 있을 수 도 있는데, 이러한 경우에는 테스트 케이스를 통해서 해당 클래스 또는 클래스에 속해있는 메소드들을 독립적으로 테스트하기가 불가능하다. 좋은 설계를 가지고 있어야지 테스트를 할 수 있다. 따라서 기본적인 것들을 준수해서 코드를 작성 할 수 밖에 없게 된다.

  • 코딩 생산성
    -아키텍처의 변경뿐만 아니라 리팩토링(기능의 변경 없이 코드의 재배치등 코드의 일부를 수정하는 작업 , ex : 메소드 이름 변경 / 메소드 분리 / 메소드 합치기 등등) 같은 경우도 많은 에러를 수반하게 되는데 ,만약 이를 위한 테스트가 잘 구현된다면 확신을 가지고 계속 변경을 진행 할 수 있다는 장점이 있어서 생산성이 증가한다.

  • 협업 촉진
    -왜냐햐면 유닛 테스트로 예시를 들어보면 , 테스트라는 것은 기본적으로 어떤 API가 있을 때 그 API가 내가 의도했던 대로 동작하는지를 체크하는 것이다. 즉, 유닛 테스트 내부에서 테스트 코드가 테스트 대상이 되는 함수를 어떻게 호출하고 있는지를 보면 해당 함수에 대한 올바른 사용방법 , 올바르지 않은 사용방법을 알 수 있다. 그럼 이런것들이 문서화가 되면서 테스트 케이스만 봐도 테스트의 기능 또는 의도등을 단번에 파악하는 것이 가능하다. 이렇게 되면 자신이 담당했던 코드가 아니더라도 손쉽게 코드를 수정 할 수 있다는 장점이 있다.

테스트 범위의 구분 : 테스팅 피라미드

-Unit Test(70%) , Interation Test(20%) End to end Test(10%) 정도의 테스트 비율로 테스틀 하는 것이 좋다고 구글에서 권장하고 있다. 또한, 테스트의 종류를 나누는 것에 정답은 없으므로 현재 내 앱의 코드 상황에 따라 달리하면 된다. 반드시 7 : 2 : 1의 비율로 테스트를 진행 할 필요는 없다.

  • UnitTest
    -코드의 가장 작은 단위 , 예를 들면 함수 등을 독립적으로 검증하는 테스트를 말한다.
    -특정 모듈(기능)이 의도대로 정확히 작동하는지 확인하는 테스트를 말한다.
    -빠르고 자주 실행 할 수 있어서 개발 초기에 문제를 발견하고 수정하는 데 도움이 된다.

  • Interation Test
    -여러 모듈 또는 서비스가 함께 작동할 때 상호 작용하는 부분을 테스트한다.

  • End to end Test
    -실제 사용자 시나리오를 기반으로 테스트한다. 즉, 사용자와의 상호작용해서 발생 하는 것들을 테스트한다.

Android Test 종류

Loacl Test(:JUnit)

-로컬 테스트란 , 기본적으로 JVM 위에서 동작하는(실제 디바이스 또는 에뮬레이터에서 동작하는 것이 아니라 현재 JUnit Test만 돌릴 수 있는 환경위에서 돌아간다는 의미) 경우 장점은 시뮬레이터 또는 실기기에서 테스트를 한다면 리소스를 등록하고 다시 해제하는 등의 작업에서 발생하는 오버웨이트 문제점이 JVM위에서는 발생하지 않는다는 것이다.

  • 스몰 로컬테스트 : 작은 함수 단위 또는 아주 작은 메서드와 연계되는 작은 테스트들을 테스트 하는 것

  • 빅 로컬테스트 : 여러 개의 인터랙션을 통해서 일어나는 여러 클래스들의 상호작용을 통해서 하나의 테스트를 위한 하나의 케이스를 테스트 하는 것

Instrumented Test(:Espresso)

-Instrumented Test란, 에뮬레이터 또는 실제 안드로이드 기기에서 테스트를 실행하는 것으로 Espresso 라이브러리를 사용한다.

-안전성 / 호환성 테스트는 반드시 실제 안드로이드 기기에서 진행하여야 한다. 하지만, 단위 테스트 같은 경우는 가능한 JVM에서 실행해야 한다.

무엇을 테스트 해야 하지.......?

테스트 대원칙

-테스트는 실제 발생 할 수 있는 에러를 예방할 수 있게 하기 위한 것이기 때문에 의미 없는 테스트를 막 추가하지 말고 의미있는 테스트를 하여야 한다.

-Edge cases를 통해 좋은 테스트 설계를 할 수 있다.

  • Boundary condition : 입력 값이 범위에 해당 할 때 입력 값 범위 밖에서 발생하는 에러를 찾을 수 있도록 테스트를 해야한다.

  • 네트워크 에러 : 미리 네트워크 테스트를 안해서 나중에 문제가 생기는 경우가 많은데 , 404 또는 500이 뜨는 에러가 발생할 수 있으므로 대처하는 방안을 마련할 수 있도록 해야한다.

  • 잘못된 데이터 : rest api를 통해서 데이터를 받았을 때 json 데이터 포맷이 깨져서 나오는 경우도 있는데 이런 데이터 타입에 관련된 테스트를 해야한다.

  • 중요한 객체가 사라졌다가 다시 생성되는 것도 가정해서 테스트 해봐야 한다. 예를 들어 화면의 configuration 또는 다크모드 밝음모드 변경 등을 말한다.

테스트 학습 순서 1단계

-테스트 구현에 익숙하지 않다면 처음에 시작 할 수 있는 것은 두가지로 경우에 학습해볼 수 있다.

작은 , 독립적인 부분 부터 시작

-독립함수 , 다른 곳과 의존성이 무조건 없는 코드에 대한 JVM 단위 테스트

큰 부분부터 시작

-핵심 시나리오에 대해서 E2E Test를 하나 만들고 해당 테스트에 들어가는 요소들을 잘게 쪼개보는 것이다.

-예를들면 , 로그인을 하는 하나의 유스케이스가 있을 때 로그인에 대한 올바른 개인정보를 넣은 후 서브 미션을 눌렀다고 하면 성공이 나올텐데 거기서 수행되는 작어비 서버로 보내는 것이 있을 수 도 있고 , 성공했을 때 로컬데이터에 어떻게 저장하는가가 있을 수 도 있고 , 이런 것들을 테스트 해보는 것이다.

테스트 학습 순서 2단계

-실제/의사 디버깅 과정에서 테스트 코드를 적용해보는 것이다.

-예를 들어 서버에 무언가를 보내는 것을 테스트 한다고 가정했을 때 사용해볼 수 있는 방법이 3가지가 있다.

실제 코드를 작성

-예를들어 실제로 테스트 케이스 내부에서 어떤 호출을 해서 실제로 서버로 보내서 받아서 결과를 확인해보는 것이다.

페이크

-페이크란 실제와 거의 동일한 역할을 하는 것을 말한다. 예를들어 테스트 케이스 내부에서 실제로 서버에 보내는 것을 메서도의 호출을 통해서 보내는 것이 아니라 정해진 데이터를 받아 적어서 서버로 보내보는 것이다. 이렇게 하면 실제 코드를 작서하는 것에 준하는 신뢰성을 갖고 있다. 물론 결과물이 진짜 결과물은 아니다.

가상의 오브젝트(Mock/Spy/Stub)

-실제 객체의 행위를 모방한 테스트를 위한 객체를 만들어서 사용하면되는데 , 이는 실제 로직을 수행하는 대신 미리 정해진 답을 반환한다. 이 가상의 객체는 테스트 하려는 코드와 상호작용하는 객체이다. 즉 가상의 객체를 사용하여서 외부 시스템이 필요 없게 만들어서 테스트를 격리시켜서 활요하는 것이다.

테스트 원칙

-테스트라고 하는 것은 실제로 일어나는 것을 그대로 반영해야 한다. 즉, 테스트가 실패하면 실제로도 해당 코드의 실행이 실패해야 되고 , 테스트가 성공하면 실제로도 해당 코드의 실행이 성공해야 된다. 만약 테스트를 잘 못 만든다면 테스트 대상 코드에 문제가 없는데도 실패하거나 테스트 대상 코드에 문제가 있는데도 성공하는 불상사가 발생한다. 따라서 이러한 일들을 예방하기 위해서 실제코드를 사용해서 테스트를 진행하는 것이 좋다. 그러나 반드시 100% 무조건 실제코드를 사용하면 안되고 웬만하면 실제코드를 사용해서 테스트 하는 것이 좋다. 왜냐하면 네트워크 같은 경우는 어쩔 수 없는데 왜냐면 네트워크를 테스트 케이스에서 활용하면 의존성이 외부에 생겨서 만약에 네트워크에서 문제가 생기면 해당 테스트 코드에 이상이 없는데도 실패가 나올 수 있기 때문이다.

테스트 작성시 도움되는 참고사항 들

-type-safe한 matcher를 적극 활용하기 : hamcrest , truth 등등 + built-in matcher

  • cf : matcher란 테스트 코드에서 예상되는 값과 실제 값을 비교하는데 사용되는 객체
  • JUnit의 내장 Matcher들을 활용해서 실패 시 실패 원인에 대한 디버깅이 편리해.
  • 장점 1. 타입이 안전하다.
  • 장점 2. 만약 테스트가 실패했을 때 로그에서 왜 실패했는지 알려준다.

-interaction 보다 state를 체크하기

  • 어떤 함수의 결과로 인해서 일어나는 상태의 변화를 검증하는 것이 , 다시 또 다른 함수의 호출로 인해서 얻는 것보다 전 후 코드를 확인하는 것이 좋다.

-shared code를 적절히 사용하기

  • shared code를 사용해서 코드 중복을 줄이고 유지 보수성을 향상 시킬 수 있다.
  • 단, 상태는 가급적이면 사용하지 않게 로직을 공유하는 것이 좋다.

-복잡한 경우에는 mocking 하지말기(특히 Context)

  • 복잡한 객체를 mocking하는것은 테스트 시 실제 동작 환경을 반영하지 못할 수 있다.
profile
포기하지 말기

0개의 댓글