이번 영상에서는 안드로이드의 테스트 자동화 기초에 대한 이론을 다루어 보도록 하겠습니다.
들어가기
테스트는 소프트웨어 개발과 함께 시작된 행위니만큼 긴 역사를 가지고 있습니다. The History of Software Testing에서는 소프트웨어 테스팅이 1822년 찰스 베비지의 차분 엔진 제작과 함께 시작되었다고 하고, 버그라는 단어는 토마스 에디슨이 1878년 동료에게 보내는 편지에서 처음 사용된 것으로 알려져 있습니다. 따라서 테스트 방법론에서는 서로 대립하는 수많은 주장이 긴 시간동안 개발되어져 왔습니다. 테스트 유형이 피라미드 구조를 가져야 한다는 사람도 있고 아이스크림 콘 형태를 가져야 한다는 사람도 있습니다. 테스트 커버리지는 100%를 목표해야 한다는 사람도 있고 혹은 100%를 목표하는 것은 무의미한 일이라고 하는 사람도 있습니다. 테스트를 먼저 만들고 프로덕션 코드를 만들어야 한다는 주장도 있고, 실무중에 그러한 방식으로 일을 할 여유가 없다는 사람도 있습니다.
모든 주장은 나름의 논리가 있기 때문에 무엇이 옳고 그른지는 알 수 없습니다. 그래서 이 파트에서는 방법론보다는 기술적으로 안드로이드에서 코드 테스트를 어떻게 수행하는지에 대해 중점을 두고 설명하도록 하겠습니다.
테스트 자동화의 필요성
안드로이드 앱의 기능을 작성하면 필연적으로 기능의 테스트를 수행하게 됩니다. 코드를 수정하고 앱을 빌드한 뒤 에뮬레이터로 앱을 실행하고 화면을 터치해 보면서 내가 만든 기능이 기대한대로 작동하는지 확인하게 되지요. 이 때 다음과 같이 다양한 유형의 테스트를 수행해야 합니다.
- 기능 테스트 : 내 앱이 기능을 수행합니까?
- 성능 테스트 : 빠르고 효율적으로 수행합니까?
- 접근성 테스트 : 접근성 서비스와 잘 작동합니까?
- 호환성 테스트 : 모든 기기와 API 수준에서 잘 작동합니까?
하지만 앱의 기능이 많아지고 구조가 복잡해지게 되면 점점 빌드 시간도 증가하게 되므로 코드를 수정할 때마다 개발자가 수동으로 앱의 모든 기능을 테스트하는 것은 물리적으로 불가능하게 됩니다. 앱의 기능을 수동으로 테스트하는 전문 QA팀을 운용할 수도 있겠지만 개발 비용이 많이 증가하게 되겠지요.
컴퓨터가 테스트를 자동으로 수행하게 하는 것을 테스트 자동화라고 하는데요, 개발에 테스트 자동화를 적용하면 기능을 개발할 때마다 일일이 빌드를 수행할 필요가 없어 개발 시간을 줄여주고, 버그를 빠른 시점에 파악할 수 있어 개발 비용을 줄일 수 있습니다. 또한 기능을 개발할 때 자동 테스트가 가능한 구조가 되도록 고려하게 되기 때문에 자연스럽게 프로그램이 더욱 견고한 구조를 갖게 되는 많은 장점이 있습니다.
자동 테스트의 분류
소프트웨어를 테스트하는 방법은 크게 블랙박스 테스트와 화이트박스 테스트로 나눌 수 있습니다. 블랙박스 테스트는 소프트웨어의 내부 구조나 작동 원리를 모르는 상태에서 소프트웨어의 동작이 요구사항을 만족하는지 검사하는 방법으로 개발자가 아닌 사용자 입장에서의 테스팅입니다. 화이트박스 테스트는 블랙박스 테스트와는 반대로 소프트웨어의 내부 구조와 제어 흐름을 직접 관찰하는 테스트 방식으로 개발자 입장에서의 테스팅이 되겠습니다. 안드로이드의 테스트 자동화에는 이 두 방식이 모두 사용됩니다.
구글은 안드로이드의 자동 테스트를 다음 그림과 같이 단위 테스트 (Unit Test), 통합 테스트 (Intergration Test), 종단간 테스트 (End-to-end Test)로 분류하고 있습니다. 단위 테스트에서 종단간 테스트로 갈수록 각 테스트의 충실도는 증가하지만 실행 시간과 유지보수 및 디버깅에 드는 노력도 증가하게 됩니다. 따라서 일반적으로는 단위 테스트를 통합 테스트보다 많이 작성하고 통합 테스트를 종단간 테스트보다 더 많이 작성하게 됩니다.
단위 테스트 (Unit Test)
단위 테스트는 앱의 메서드 또는 클래스와 같은 작은 단위의 기능을 검증합니다. 테스트 범위가 작기 때문에 작성이 수월하고 테스트 속도가 빠릅니다.
- 로컬 단위 테스트 (Local Unit Test)
Android Framework에 의존하지 않고 로컬 JVM에서 실행되는 테스트입니다. 실제 기기나 에뮬레이터를 사용하지 않으므로 테스트 속도가 빠르다는 특징이 있습니다.
- 계측 단위 테스트 (Instrumented Unit Test)
Android Framework에 의존하는 기능을 검증하는 단위 테스트입니다. 실제 기기 또는 에뮬레이터에서 실행해야 하기 때문에 실행시간이 오래 걸린다는 특징이 있습니다.
통합 테스트 (Intergration Test)
통합 테스트는 서로 다른 모듈 또는 클래스 간의 상호작용이 정상적으로 기능하는지를 검증합니다. 단위 테스트로 각 모듈의 기능을 검증했을지라도, 동시에 여러 모듈을 동작시켰을 때는 정상작동을 하지 않을 수도 있기 때문입니다.
종단간 테스트 (End-to-end Test)
종단간 테스트는 앱의 전체 화면 또는 여러 모듈에 걸친 사용자 흐름과 같은 큰 부분에 대한 기능 검증을 수행합니다. User Interface를 사용할 때 화면이 올바르게 응답하는지 확인하는 테스트이기 때문에 UI 테스트라 부르기도 합니다.
테스트 사이즈
단위, 통합, 종단간이라는 분류는 얼핏 듣기에 모호하게 들릴 수 있습니다. 그래서 구글은 사이즈 기준으로 단위 테스트는 소형 테스트, 통합 테스트는 중형 테스트, 그리고 종단간 테스트는 대형 테스트로 분류하기도 합니다. 크기에 따른 각 테스트의 특징은 다음 표와 같습니다.
Feature | Small | Medium | Large |
---|---|---|---|
Network access | No | localhost only | Yes |
Database | No | Yes | Yes |
File system access | No | Yes | Yes |
Use external systems | No | Discouraged | Yes |
Multiple threads | No | Yes | Yes |
Sleep statements | No | Yes | Yes |
System properties | No | Yes | Yes |
Time limit (seconds) | 60 | 300 | 900+ |
테스트 기본 원칙
이번에는 프로그램을 테스트할 때 지켜야 하는 몇가지 원칙을 소개하겠습니다.
Seven Testing Principles
Seven Testing Principles은 ISTQB (International Software Testing Qualifications Board)에서 제안한 7가지 테스트 원칙입니다.
- Testing shows the presence of defects, not their absence : 테스팅은 결함의 존재를 보여주는 것이다.
- Exhaustive testing is impossible : 완벽한 테스트는 불가능하다.
- Early testing saves time and money : 테스트 구성은 가능한 빠르게 시작한다.
- Defects cluster together : 결함은 군집되어 있다.
- Beware of the pesticide paradox : 살충제 역설 - 비슷한 테스트가 반복되면 새로운 결함을 발견할 수 없다.
- Testing is context dependent : 테스팅은 문맥에 의존적이다.
- Absence-of-errors is a fallacy : 오류 부재의 궤변 - 사용되지 않는 시스템이나 사용자의 기대에 부응하지 않는 기능의 결함을 찾고 수정하는 것은 의미가 없다.
F.I.R.S.T Principles
F.I.R.S.T Principles이란 로버트 마틴 선생의 Clean Code에서 소개한 단위 테스트에 관한 원칙들을 Brett Schuchert과 Tim Ottinger가 F.I.R.S.T라는 이름으로 정리한 것입니다.
Fast
: 단위 테스트는 빨라야 한다.Isolated
: 테스트가 다른 테스트 케이스에 의존하지 않아야 한다.Repeatable
: 테스트는 실행할 때마다 같은 결과를 만들어야 한다.Self-validating
: 테스트 결과는 성공이거나 실패여야 한다. 결과에 대한 해석이 필요해서는 안된다.Timely
: 단위 테스트는 기능이 출시된 후에도 언제든 작성할 수 있지만 적절한 시점은 프로덕션 코드를 구현하고 있는 와중에 테스트 코드를 작성하는 것이다.
테스트 코드 작성 스타일
테스트의 원칙은 알겠습니다. 그럼 테스트 코드는 어떤 스타일로 작성해야 할까요? 넓은 공감대를 얻고 있는 스타일 중의 하나는 마틴 파울러 선생이 제안한 Given-When-Then 스타일입니다. GWT는 테스트 코드 구조를 다음 세 부분으로 분리해서 생각합니다.
Given
: 어떠한 상태하에서When
: 어떠한 기능을 실행하면Then
: 어떠한 결과가 나와야 한다.
예를들어 x = 1일 때(Given), 2를 곱하면(When), 2가 나와야 하는(Then) 테스트 메소드는 다음과 같은 구조를 갖게 되는 것입니다.
|
|
Test Double
영화 촬영에서 우리가 흔히 아는 스턴트맨의 정확한 용어는 스턴트 더블이라고 하는데요, 테스트 더블은 말그대로 실제 구성요소 대신 테스트를 수행하는 객체입니다. xUnit Test Patterns의 저자인 제라드 메스자로스(Gerard Meszaros)가 만든 용어라고 하네요.
F.I.R.S.T Principles중 Isolated
에 따르면 테스트는 다른 테스트에 의존하지 않고 격리되어서 어떤 순서로든, 어떤 개별 테스트이든 실행할 수 있어야 합니다. 격리한다는 것은 테스트 대상이 의존하는 것을 실제가 아닌 다른 것으로 대체하는 것인데 이 때 사용하는 것이 테스트 더블인 것입니다. 테스트 더블에는 다음과 같이 Dummy, Stub, Spy, Mock, Fake 가 있는데요, 각각의 정의와 사용법에 대해 설명하도록 하겠습니다.
Dummy
가장 기본적인 테스트 더블로 기능을 전혀 구현하지 않은 객체입니다. 인스턴스는 필요하지만 기능까지는 필요하지 않은 경우에 사용합니다. DummyAccountDao
는 AccountDao
를 상속하고 있지만 기능을 구현하지 않았죠.
|
|
Stub
Dummy가 실제로 동작하는 것처럼 보이게 만들어 놓은 객체입니다. 인터페이스 혹은 클래스를 최소한으로 구현하고, 결과는 특정 상태를 가정해서 하드코딩된 값을 제공합니다. 따라서 로직에 따른 값의 변경은 테스트 할 수 없습니다. 객체에 특정한 메소드를 적용하기 전과 후의 상태를 확인하는 상태 기반 테스트(State-based testing)에 사용되는 테스트 더블입니다. StubAccountDao
는 AccountDao
를 상속하지만 showMember
처리까지 완전히 구현하는게 아니라 단순히 고정된 상수값을 반환하도록 하고 있습니다.
|
|
Spy
Stub과 비슷한 객체인데 추가적인 정보를 더 기록할 수 있게 구성되어 있습니다. 예를 들어 예상된 메소드가 잘 호출되었는지 몇 번이나 호출되었는지 등을 파악할 수 있습니다. 어떤 메소드 A가 호출 되었을 때 또 다른 메소드 B가 실행이 되었는지 확인하는 등의 행위 기반 테스트 (Interaction-based testing)에 사용되는 테스트 더블입니다. SpyAccountDao
의 showMember
메소드를 보면 내부에서 callMemberCount
를 증가시키는 스파이 동작을 하는 것을 알 수 있습니다.
|
|
Fake
동작하는 구현이 있기에 테스트에는 사용할 수 있지만 프로덕션 구현과 동일하지는 않은 객체입니다. 상태 기반 테스트(State-based testing)에 사용되는 테스트 더블인데, Mocking을 필요로 하지 않고 가볍기 때문에 구글에서 추천하는 더블입니다. FakeAccountDb
는 AccountDb
를 가짜로 구현하는데, Stub과 차이점이라면 Stub의 경우 메소드 로직은 구현하지 않고 단지 고정값을 반환하지만, Fake는 메소드 로직까지 구현한다는 점이 다릅니다.
|
|
Mock
Mock은 Spy처럼 행위 기반 테스트에 사용되는 테스트 더블입니다. 차이점은 Spy가 메소드 A와 B의 실제 동작을 추적하는 더블인데 반해, Mock은 A가 실행되면 그냥 B의 정상 작동값을 반환하도록 구성된 더블이라고 생각하시면 됩니다. 행위 기반 테스트는 작성이 복잡하고 까다로운 부분이 많기 때문에 Mocking에 사용하는 전용 라이브러리들이 만들어져 있습니다. 코드를 보시면 Car
는 엔진과 대시보드 의존성을 주입받도록 되어 있고 CarTest
에서 Car
를 테스트 합니다. 이때 엔진과 대시보드의 Mock을 만들어서 주입한 뒤 Mock이 정상적으로 작동하는지를 verify
로 검증하는 방식입니다.
|
|
테스트 더블은 기능을 구현한 정도에 따라 다음과 같이 구분할 수도 있습니다.
테스트 대상 항목
구글에서 제안하는 앱의 주된 테스트 대상 항목은 다음과 같습니다.
- 단위 테스트 대상
- ViewModels 또는 Presenter
- 데이터 레이어
- 유즈 케이스
- 값을 계산하는 유틸리티 클래스
- 엣지 케이스
- 음수, 0 및 경계 조건 을 사용하는 수학 연산
- 가능한 모든 네트워크 연결 오류
- 형식이 잘못된 JSON과 같은 손상된 데이터
- 파일 저장시 스토리지가 가득 찬 상황
- 프로세스 중간에 다시 생성된 개체(예: 장치가 회전할 때의 액티비티)
- UI 테스트
- 스크린 UI
- 유저 플로우
- 내비게이션
- 테스트 제외 대상
- 프레임워크 자체의 동작
- activities, fragments, 혹은 services에는 테스트가 필요한 로직을 가능한 배치하지 않음
테스트 라이브러리
안드로이드에서 사용할 수 있는 테스트용 라이브러리에 대해 알아보도록 하겠습니다.
Testing Framework
- JUnit4 : Java의 단위 테스트 코드를 작성하기 위해 만들어진 프레임워크로 Jetpack Test 라이브러리는 JUnit4를 기준으로 만들어져 있음. 현재 최신 버전은 JUnit5이나, 안드로이드를 완벽히 지원하지 않음.
- Kotest : Kotlin의 단위테스트 코드를 작성하기 위해 만들어진 프레임워크.
- Roboletric : JVM 만으로 안드로이드 프레임워크를 테스팅하기 위해 만들어진 프레임워크.
Assertion
Assertion의 표현력이 부족한 JUnit의 단점을 메워주는 여러가지 라이브러리들이 있습니다. AssertJ와 Truth는 사용법이 비슷한데 라이브러리를 관리하는 구글에 따르면 Truth가 AssertJ보다 먼저 만들어졌다고 하네요.
|
|
Mocking
- Mockito : Java 테스트를 위한 Mocking 라이브러리
- MockK : Kotlin 테스트를 위한 Mocking 라이브러리
UI Testing
- Espresso : 단일 안드로이드 앱의 UI를 테스트하는 프레임워크
- Kaspresso : KasperskyLab에서 제작한 안드로이드 앱 UI 테스트 프레임워크
- Appium : iOS와 Android를 모두 테스트 할 수 있는 UI 테스트 프레임워크
- UI Automator : 복수 앱 간의 UI 기능을 테스트하는 프레임워크
구글의 안드로이드 테스팅 환경
Testing Related Video from Android Developers
구글은 테스팅의 중요성을 지속적으로 강조해왔고 테스팅 전략이나 새로운 테스팅 라이브러리를 꾸준히 발표해 왔습니다.
- Android Testing (Android Dev Summit 2015)
- Test-driven development on Android with the Android Testing Support Library (Google I/O ‘17)
- Frictionless Android testing: Write once, run everywhere (Google I/O ‘18)
- Testing rebooted (with AndroidX Test) (Android Dev Summit ‘18)
- Testing Android apps at scale with Nitrogen (Android Dev Summit ‘18)
- Build testable apps for Android (Google I/O'19)
- What’s new in Android testing tools | Session
Jetpack Test Library
안드로이드 모듈의 테스트를 위한 핵심 라이브러리가 바로 Jetpack Test 라이브러리 입니다. 저장소는 Github에도 공개되어 적극적인 피드백을 받고 있는데, 다음과 같은 아티팩트들로 구성되어 있습니다.
Artifact | Stable Release |
---|---|
annotation | 1.0.0 |
core | 1.4.0 |
espresso | 3.4.0 |
ext.junit | 1.1.3 |
ext.truth | 1.4.0 |
monitor | 1.5.0 |
orchestrator | 1.4.1 |
runner | 1.4.0 |
rules | 1.4.0 |
services | 1.4.1 |
UI/Application Exerciser Monkey
안드로이드 앱은 UI/Application Exerciser Monkey를 통해 몽키 테스트를 수행할 수 있습니다. 사람이 아닌 원숭이는 앱에 대해 아는게 없으니까 아무거나 막 눌러보겠죠. 그런 임의 동작을 통해 이슈를 찾아내는 테스트 방법을 몽키 테스트라고 합니다.
Monkey는 에뮬레이터 또는 장치에서 실행되고 클릭, 터치 또는 제스처와 같은 사용자 이벤트의 의사 무작위 스트림과 여러 시스템 수준 이벤트를 생성합니다. ADB를 통해 다음과 같이 실행하며 여러가지 옵션을 지정할 수 있습니다.
|
|
Unified Test Platform
Android Studio와 Android Gradle Plugin에서 Android Instrument Test Runner 버전이 다를 경우 Android Studio와 CI서버의 테스트 결과가 다르게 보고될 수 있습니다.
이런 문제를 해결하기 위해 AGP 7.1.0 및 Android Studio Bumblebee 이상의 버전에서는 모든 계측 테스트가 통합된 Gradle Test Runner에서 실행되도록 변경되었습니다.
구글은 여기에서 한걸음 더 나아가 Unified Test Platform이라는 것을 발표했습니다. 처음에는 Nitrogen이라는 이름으로 2018년에 발표된 프로젝트인데 2021년 Google I/O에서 처음으로 실물이 공개되었습니다. Jetpack의 연료라는 의미에서 Nitrogen이라는 이름을 붙였다고 하네요. UTP를 이용하면 서로 다른 OS의 테스트를 모듈 방식으로 추가할 수 있고 병렬로 테스트를 수행할 수 있어서 테스트를 대규모화할 수 있게 된다고 합니다.
Code Coverage
내 코드가 얼마만큼 테스트 되고 있는지 평가할 수 있는 지표를 Code Coverage라고 합니다. Java 코드를 평가하는 데에는 Java Code Coverage Library(JaCoCo)가 사용되는데요, 안드로이드 스튜디오에는 이 기능이 기본적으로 포함되어 있기 때문에 코드 테스트를 실행할 때 Run
대신 Run test-name with coverage
메뉴로 실행하면 다음과 같이 Code Coverage 결과를 얻을 수 있습니다.
앱의 모든 코드가 테스트 되고 있다면 Code Coverage는 100%가 될 텐데요, Code Coverage 100%를 달성하는 것은 물리적으로도 매우 어려운 일일 뿐더러, 실제로는 테스트를 하지 말아야 하는 종류의 코드도 있기 때문에 100%라는 숫자를 달성하는 것에 매달릴 필요는 없습니다. Test Driven Development의 저자인 Kent Beck 선생은 코드 커버리지 100%를 뽐내는 사람은 신문의 글자 하나 하나를 읽었다고 자랑하는 것과 같다고 비판하기도 했습니다.
어떤 코드가 테스트 하기 좋은 코드인가?
어쨌든 Code Coverage를 높이기 위해서는 더 많은 기능에 대한 테스트를 만들어야 합니다. 그럴려면 각 기능이 테스트를 하기 좋은 구조로 되어 있어야 하겠죠. 그렇다면 테스트를 하기 좋은 코드라는 건 어떤 것일까요?
이 부분은 간단히 설명할 수 있는 주제가 아니기 때문에 대신 인사이트를 얻을 수 있는 훌륭한 세미나 발표를 하나 소개드리도록 하겠습니다. OKKY가 TDD를 주제로 2018년에 주최한 OKKYCON: 2018 - The Real TDD라는 세미나가 있는데요, 당시 발표자료가 유튜브에 공개 되어 있기 때문에 참고하시면 좋을 것 같습니다.
CI/CD
마지막으로 CI/CD에 대해 알아보도록 하겠습니다. Build tool과 Android Gradle Plugin
강의에서 설명한대로 프로덕트의 빌드는 다음과 같은 과정을 거치게 됩니다.
- 연관된 의존성 다운로드
- 소스 코드를 바이너리 코드로 컴파일
- 바이너리 코드 링크하여 실행가능 파일로 패키징
- 테스트 수행
- 프로덕션 시스템에 배포
다시 말해 개발자는 코드를 작성할 때마다 컴파일을 하고 테스트를 수행한 뒤 서버에 업로드해야 합니다. 코드 변경이 별로 없는 프로덕션이라면 빌드를 개발자가 직접 수행해도 문제 없겠지만, 코드 변경이 10분마다 한 번씩 일어난다면 빌드를 수행하다가 하루가 끝나버릴 것 같네요. 만약 수십명의 개발자가 기능을 개발하는데 연관되어 있다면 이젠 어떤 순서로 빌드를 수행해야 할 지도 알 수 없게 되어 버릴 것입니다.
지속적 통합(Continuous Integration, CI)은 개발자가 코드를 작성하고 저장소에 커밋하면 자동으로 컴파일과 테스트를 수행하고, 여러 개발자의 커밋에 대한 빌드 순서도 관리해 주는 솔루션을 의미합니다. 실무에서 많이 사용되는 솔루션으로는 Github Actions, CircleCI, Bitrise, Jenkins, TeamCity 등이 있습니다.
지속적 제공(Continuous delivery, CD)은 CI가 완료되어 배포할 수 있는 상태의 빌드 결과물을 저장소에 업로드 하는 것이고, 지속적 배포(Continuous Deployment, CD)는 지속적 제공 과정이 완료된 결과물을 최종 사용자가 사용할 수 있는 환경까지 자동으로 배포하는 솔루션을 의미합니다. 실무에서 많이 사용되는 솔루션으로는 Fastlane, Firebase App Distribution, DeployGate, Bitrise Ship 등이 있습니다.
CI와 CD의 진행구성은 다음 그림과 같습니다. CI/CD를 도입하면 개발자는 빌드와 배포에 대해서는 신경쓰지 않고 코드를 작성하는데에만 집중할 수 있습니다. 테스트 자동화는 CI/CD 솔루션과 결합될 때 진정한 위력을 발휘할 수 있게 되는 것입니다.
이렇게 해서 안드로이드의 테스트 자동화 기초에 대해 알아보았습니다.