안드로이드/잡다한 지식

드로이드나이츠 2021 테스트코드편

비비디바비비부 2022. 2. 20. 08:35

[드로이드나이츠 2021] 강사룡 - Android Testing Best Practices - YouTube

드로이드나이츠 2021

Android Testing Best Practices

Why should I write test code

  • 코드 생산성 증대: 확신을 갖고 시스템을 수정할 수 있음.
  • 효율적으로 버그를 잡을 수 있음
  • 코드 변경(특히 Refactoring)을 쉽게 할 수 있다.
  • modular한 설계에 도움: Single responsibility

Why should we write test code?

  • 협업을 위해 반드시 필요
    • 여름휴가 중 내 코드에서 장애가 난다면?
      • or 몇 개월 뒤의 내가 갑자기 내 코드를 다시 보게 된다면?
    • 협업을 촉진: 코드 주인이 아니더라도 코드를 수정할 수 있다.
      • 문서로서의 테스트코드. 테스트 케이스만 보면 특정 시스템의 기능과 의도, 올바른 사용법을 단번에 파악 가능
      • 원래 의도와 어긋나게 수정했다고 해도 테스트가 실패하므로 금방 알아챌 수 있다.
      // 4분 29초

구글의 테스트 정책

  • 높은 Test Coveratge -> 커버리지는 테스트 코드가 전체 코드의 몇퍼센트를 커버하고 있냐라는 뜻
  • 비욘세 룰 - 코드를 만드는 사람이라면 그 코드에 대한 테스트 코드도 짜주는게 예의 아니야?

When to use real device for testing

  • DO - 안전성 / 호환성 테스트, 성능 테스트
  • 격리된 테스트 환경 구축 - 외부 서버에 가야하는 경우 일단 앱에서만 테스팅

테스트 코드를 처음/다시 해본다면? - 1단계

  • 작은, 독립적인 부분부터 시작 -> Ex) 계산/ 변환 로직을 가진 클래스
  • 책 추천 (테스트주도개발)

2단계 실제로 도움이 되는 테스트 코드 작성

  • 실제/의사 디버깅 과정에서 테스트 코드 적용
    • 구글의 경우, 장애가 발생하면
      • i. 문제가 된 PR를 찾아서 롤백
      • ii. 문제 PR에 장애를 재현하기 위한 테스트 코드를 작성 -> 그 테스트는 fail
      • iii. 테스트가 성공하도록 코드를 수정

실제에 가까운 단위 테스트 만들기

  • 의존성은 어떻게 해결하나?
    • 예: SQLite, REST/gRPC call
    • 1순위: 의존성 관계에 있는 진짜 코드를 사용 예 : in-memory DB
    • 2순위: 라이브러리에 의해 제공되는 표준 fake를 사용
    • 3순위: 위의 방법이 불가능할 때, mock 사용

Mocking Best Practice

  • type-safe한 matcher를 활용할 것
  • interaction보다 state를 체크할 것
  • 필요시 shared code를 적절히 사용할 것
  • Android API를 mocking하지 말 것
    • Robolectric! via androidx.test
    • 프래그먼트 독립 생성, Life Cycle 제어 등 많은 개선이 있었음

3단계: 패자부활전!

  • 테스트 코드의 유지보수가 어렵다
  • 이전에 잘 돌아갔던 테스트가 어느 순간 fail
  • Brittle test -> 깨지기 쉬운 테스트

대원칙: Unchanging Test

  • Test should not be changed by following reasons:
    • 리팩토링 시 테스트코드가 변화 x
    • 버그 픽스 시 테스트 코드 변화 x
    • New features 시 테스트코드 변화 x
  • 의도가 변경 시 테스트 코드 변경됨

Best Practice

fun processTransaction(transaction: Transaction) {
    if(isValid(transaction)) {
        saveToDatabase(transaction)
    }
}

@Test
fun shouldNotPerformInvalidTransactions() {
    processor.setAccountBalance("me", 50)
    processor.setAccountBalance("you", 20)

    processor.processTransaction(newTransaction()
                                .setSender("me")
                                .setRecipient("you")
                                .setAmount(100))
    assertThat(processor.getAccountBalance("me")).isEqualTo(50)
    assertThat(processor.getAccountBalance("you")).isEqualTo(20)
}