유닛 테스트의 완전성(exhaustivity)과 취약성(fragility)
1️⃣ 요약
저자는 The Composable Architecture(이하 TCA)가 추구하는 유닛 테스트의 완전성이 TDD의 안티패턴이라고 주장하고, 이를 보완하기 위해 TCA를 fork하여 완전성을 강제하지 않는(non-exhaustive) TestStore 객체를 추가했습니다. 무슨 문제가 있었던걸까요?
TCA의 개발자가 테스트의 완전성에 높은 가치를 두었기 때문에 TCA에서 유닛 테스트를 작성할 때는 검증하고자 하는 것과 관련된 모든 action, state, effect를 복제해야 합니다. 그래서 보통 하나의 테스트 케이스에서 하나의 action을 테스트하는 식으로 코드를 작성하게 되죠. 그러나 이런 테스트는 매우 취약(fragile)합니다. 테스트가 취약하다는건 구현부(implementation detail)를 고쳤을때 테스트까지 고쳐야하는 상황을 의미합니다.
테스트가 취약하면 여러 문제가 발생합니다. 구현부만 수정했는데도 테스트가 자주 깨지면 사람들은 코드를 자세히 들여다보지 않고 검증부를 바로 수정해버립니다. 그리고 테스트 무용론에 빠집니다.
2️⃣ 큐레이터 생각
저는 아직 현업에서 TCA를 쓰고 있진 않지만, RIBs를 썼을때 동일한 문제를 느꼈습니다. 기본 제공해주는 유닛 테스팅 템플릿을 사용할때는 보통 하나의 테스트케이스에서 하나의 인터랙터 메서드를 호출한 뒤 상태 변화와 사이드 이펙트를 검증합니다. 이때 호출되는 메서드는 보통 PresentableListener 프로토콜의 메서드고, 모킹되는 객체는 Routing과 Presentable입니다.
그러나 이런 테스트는 구현부 변경에 너무 취약합니다. UI나 유저 플로우가 그대로여도 구현부를 조금만 수정하면 테스트 코드도 수정해줘야하는 일이 빈번합니다. 저는 Riblet을 하나의 단위로 보고 Listener와 Buildable 인터페이스에 변화가 없다면 그 안에서 라우터, 인터랙터, 뷰를 아무리 수정해도 유닛 테스트는 변하지 않아야 한다고 생각했습니다. 그래야 리팩토링이 용이하고 작업 결과에 자신감을 가질 수 있기 때문에요.
하지만 이분만한 대단한 개발자는 못돼서 문제의식만 가지고 직접 해결하진 못했는데, 마침 작년에 이 문제를 해결해줄 라이브러리를 찾았습니다. Hammer(https://github.com/lyft/Hammer)를 사용해서 테스트를 짜면 Riblet의 구현부가 아무리 바뀌어도 유저 플로우가 바뀌지 않는 이상 테스트 코드를 수정할 일이 없어집니다. 현업에 도입하여 활용 중인데 예상대로 테스트의 취약성을 낮추는 효과를 톡톡히 보고 있습니다.
📝 팀 전체가 지속적으로 유닛 테스트 짜고 오랫동안 유지하려면 취약성을 경계하고 잘 관리해야 합니다.