오늘 포스팅할 내용은 "테스트 코드 작성"에 관한 이야기이다.
안드로이드 단톡방에서 사람들이 메서드 하나 추가될 때마다 적는다던 테스트 코드, 정보처리기사 공부하다 봤던 TDD,
여러 채용 공고들에 숱하게 적혀있던 "테스트 코드 작성 경험이 있으신 분" 등 안드로이드 진영에서의 테스트 코드 작성의 중요성은 날이 갈수록 커지고 있다.
"테스트 코드"를 실무에서 작성해 본 경험이 없어 이에 대한 지식이 전무한 사람들도 꽤 많을 것이라 생각되는데, 이번 "테스트 코드" 시리즈 포스팅을 읽은 뒤에는 테스트 코드 작성의 중요성과 작성 방법, 다른 여러 정보들을 알고 갈 수 있었으면 좋겠다.
테스트 코드란?
소프트웨어의 기능과 동작을 테스트하는데 사용되는 코드
테스트 코드는 비단 spring, android에서만 작성하는 것이 아니다. 소프트웨어의 모든 영역에서 작성이 적극 권장되는, 건강하고 예측 가능한 범위 내에서 관리할 수 있는 소프트웨어를 위한 필수 요소이다.
처음부터 완벽하게 짜서 테스트할 일이 없도록 만들면 안되나요? <- 이런 의문을 가지는 사람이 있을 수도 있는데, 이 사람은 아마 사이드 이펙트의 무서움을 경험해보지 못한 사람일 가능성이 크다.
사이드 이펙트는 어디에도 있으나 어디에도 없다. 이 말인 즉슨, 개발자는 소프트웨어를 100% 자신의 영역 안에서 컨트롤할 수 없다는 것이다.
아무리 뛰어난 개발자라고 한들, 결국 컴퓨터가 아닌 인간이기 때문에 휴먼에러를 피할 수 없다!!!
그럼 이제 간단한 코드를 예시로 들어 테스트 코드가 주는 안정감을 살짝쿵 느껴보자.
예시코드 (수정 전)
class DataSourceImpl: DataSource {
@Override
fun laughOutLoud(): String {
return "lol"
}
}
위 코드는DataSource 인터페이스를 구현한 DataSourceImpl 클래스이다.
laughOutLoud 메서드를 Override해서 lol 이라는 String 타입을 리턴하는 것을 볼 수 있다.
하지만 여기서 어떠한 이유로 해당 클래스와 메서드의 구현이 변경되었다고 가정해보자.
예시코드 (수정 후)
class DataSourceImpl constructor(
private val database: LocalDatabase
): DataSource {
@Override
fun laughOutLoud(): String {
return database.myDao().getLol()
}
}
수정 후의 코드는 RoomDatabase를 상속받은 LocalDatabase를 생성자 파라미터로 받고, database - myDao - getLol 을 통해 String을 리턴하는 메서드가 되었다.
수정 전의 코드는 단순히 "lol"이라는 문자열 리터럴을 반환하는 코드였으나,
수정 후의 코드는 이제 다른 여러 요소들과 상호작용하는 복합적인 구조가 된 것이다.
이제는 원래의 "예측 가능한 범위"를 벗어나 다른 클래스들의 변경이나 문제에도 영향을 받게 된다. 예측해야 하는 범위가 넓어졌으며, 발생할 가능성도 올라간 것이다.
간단하게 수정 후의 코드에서 발생할 수 있는 사이드 이펙트를 몇 개 추려보자.
여기서 발생할 수 있는 사이드 이펙트
1. RoomDatabase에 정상적으로 접근하지 못하는 경우
2. RoomDatabase에서 호출한 myDao의 쿼리문에 문제가 있는 경우
3. RoomDatabase에 정상적으로 접근하여 getLol 메서드(+쿼리)까지 수행하였으나, 그 리턴값이 없는 경우/예외 발생
깊이 들어가지 않았는데도 벌써 3개의 사이드 이펙트 발생 가능성을 탐지했다.
그래서 개발자 아무개씨는 "테스트 코드를 쓸 결심"을 하게 된다.
간단한 테스트 코드
Given("DataSourceImpl instance") {
When("laughOutLoud 호출 시") {
Then("RoomDB에서 가져온 String을 반환한다") {
// 테스트 코드 Here
}
}
}
위 테스트 코드는 When 블럭이 성공 하나뿐이나, 실 테스트 코드 작성 시에는 성공-실패-예외 모두 작성해야 한다.
하지만 이것만으로도 개발자는 A메서드는 A'로 수정했을 때, A'가 해당 테스트를 통과한다면 기존 A메서드와 호환이 된다는 이야기이므로 사이드이펙트로부터의 불안함을 조금은 덜 수 있게 된다.
또한 테스트 코드만 봐도 해당 메서드가 어떻게 짜여졌는지, 어떤 목적으로 작성되었는지를 알 수 있어 각 메서드의 "문서" 역할 또한 수행하게 된다.
테스트 코드를 작성해야 하는 이유
(1) 구글 스토어에 앱 게시
안드로이드의 경우 특히 더 중요한데, 그 이유는 한 번 스토어에 게시하면 새로 업데이트하는데에 최소 2-3시간이 소요되는 네이티브(하이브리드 X)의 특성 상 핸들링 가능한 범위를 벗어나는 것은 굉장히 리스크가 크기 때문이다.
그로 인한 사용자의 이탈은 회사의 손실로 이어지며, 그건 곧 "예측하지 못한 상황"과 그것을 컨트롤하고 예방하지 못한 개발자들에 대한 문책으로 이어진다.
(2) 테스트 코드가 곧 문서가 된다
테스트 코드에 적용하는 디자인 패턴에 따라 작은 차이가 있을 수는 있으나, 테스트 코드는 그 자체로 해당 클래스와 메서드들에 대한 문서 역할을 한다.
Given-When-Then 패턴을 적용했을 경우, 각 항목들의 로직을 보면 플로우를 더욱 쉽고 직관적으로 확인할 수 있다.
(3) 리팩토링과 사이드이펙트
이 둘은 뗄래야 뗄 수 없는 관계이다. 휴먼에러는 100% 방지할 수 없으며, 리팩토링 시에는 0%에 수렴한다고 봐도 무방하다.
하지만 테스트 코드를 작성해놓았따면 수정한 메서드가 기존 테스트를 통과하는지만 확인하면 기능이 정상적으로 작동하는지 체크가 가능하다.
테스트의 종류 - 안드로이드
(1) UI 테스트
UI가 개발자가 의도한 대로 동작하는지 확인하기 위해 시행하는 테스트이다.
주로 Espresso라는 프레임워크를 사용하는데, 거의 사용자와의 상호작용(클릭/롱클릭/슬라이드/스크롤/입출력)을 검증한다.
안드로이드 스튜디오에서는 androidTest라는 폴더 안에 테스트를 작성하게 된다.
(2) Unit 테스트(단위 테스트)
가장 일반적인 테스트라고 불리우는 단위 테스트는 가장 작은 단위의 테스트이다.
테스트할 수 있는 가장 작은 단위는 메서드이며, 주어진 환경에서 해당 메서드가 정상적으로 동작하는지, 동작의 결과값은 예상한 것과 같은지를 검증하게 된다.
Java 진영에서는 JUnit4/JUnit5와 Mockito를,
Kotlin 진영에서는 JUnit5/Kotest와 Mockk를 주로 사용한다.
"테스트 코드 작성"은 일반적으로 이 "단위 테스트"를 지칭하는 것이라고 생각하면 된다.
Testable한 코드란
😂 아 싱글톤.
😂 아 파이널.
😂 아 스태틱.
---> 추후 포스팅 예정.
테스트 코드 작성은 절대 만만하게 볼 상대가 아니다.
프레임워크의 종류도 다양하며, 생소한 요소들을 사용하는 숙련도를 키우기도 쉽지 않다.
테스트 코드 작성 시에도 적용할 수 있는 다양한 디자인 패턴들이 있는데, 이 또한 굉장히 중요하다.
그래서 원래는 하나의 포스팅에 모두 적으려 했으나, 시리즈로 변경하여 총 4-5개 정도로 나누어 적으려 한다.
추후 포스팅 예정 - 시리즈:테스트 코드
- 테스트 코드 - (2) 디자인 패턴
- 테스트 코드 - (3) 프레임워크/라이브러리
- 테스트 코드 - (4) Testable한 코드를 위한 설계
'개발 > Kotlin' 카테고리의 다른 글
[Kotlin] 타입 소거와 Star projection (0) | 2024.06.27 |
---|---|
[Kotlin] 테스트 코드 (2) - TDD/BDD와 디자인 패턴 (0) | 2023.07.02 |
[안드로이드] 테스트 코드 - JUnit의 예외 처리 (expected, assertThrows, doThrow) (0) | 2023.06.26 |
Singleton 패턴이란? (0) | 2022.12.01 |
연산자 개수에 따른 차이(&, |와 &&,||의 차이) (0) | 2022.07.25 |