우당당탕 Unity PlayMode 문제 해결 과정

글을 작성하는 지금은 해결했지만, 최근에 프로젝트에서 발생한 테스트 코드관련 버그가 있어서 매우 고생했다.

여기에 코드를 적는 것 보다 코드 리뷰에 대한 내용은 따로 링크를 걸고 블로그엔 해결 과정과 느낀점을 기록한다!

문제 발생

약 일주일전 상호작용을 구현하기 위해 테스트 코드를 작성하고 있었다.

상호작용의 경우 명확한 동작이 정해져 있어서 상호작용 성공, 실패의 경우로 테스트 코드를 작성했다.

[UnityTest]
public IEnumerator TestPlayerInteraction()
{
    GameObject interactionObject = new GameObject("InteractionTestObject");
    interactionObject.AddComponent<CircleCollider2D>();
    interactionObject.AddComponent<NpcInteraction>();
    interactionObject.layer = LayerMask.NameToLayer("Object");
    
    interactionObject.transform.position = new Vector3(1.0f, 0.0f, 0.0f);
    
    yield return null;
    
    Assert.IsTrue(_interaction.Interact(Vector2.right));
    
    GameObject.DestroyImmediate(interactionObject);
}

[UnityTest]
public IEnumerator TestPlayerInteractionFail()
{
    GameObject interactionObject = new GameObject("InteractionTestObject");
    interactionObject.AddComponent<CircleCollider2D>();
    interactionObject.AddComponent<NpcInteraction>();
    interactionObject.layer = LayerMask.NameToLayer("Object");
    
    interactionObject.transform.position = new Vector3(1.0f, 0.0f, 0.0f);
    
    yield return null;
    
    Assert.IsFalse(_interaction.Interact(Vector2.left));
    
    GameObject.DestroyImmediate(interactionObject);
}

PlayMode 테스트, 코드 구조, 상호작용에 관한 이야기는 위 코드리뷰를 참고해주세요.

처음 코드를 작성하고 두 테스트다 문제없이 성공한 것을 확인하고 바로 코드베이스에 PR을 올렸다.

그런데 협업자로부터 테스트가 실패했다는 메시지를 받았다.

바로 확인하기 위해 나도 다시 테스트를 돌렸는데, 테스트가 실패했다.

즉, 테스트 자체가 때에 따라 성공하거나 실패하는 것이였다. (약 성공이 10% 실패가 90%정도)

처음에는 원인을 몰라서 실제 씬에 따라서 만들어 보거나 Raycast의 문제로 인식하여 해당 질문글도 유니티 포럼에 올렸다.

아직까지 답변이 없다.. 물론 해결되어서 상관없지만 ㅠ

가정

해당 코드의 문제를 해결하기 위해 몇 가지 가정을 하면서 진행했다.

  • 다른 테스트 코드의 영향을 받는다.
  • 프레임이 잘 안넘어간다.
  • Raycast의 문제이다.
  • 상호작용 오브젝트가 삭제되지 않는다.
  • 유니티의 억까이다.

결론부터 말하자면, 1, 2, 4번이 맞았다.

머릿속으론 이해할 수 없는 상황이 자주 나왔는데 예를 들어 오브젝트가 (1, 0, 0)에 위치하고 인터렉션을 (-1, 0, 0)으로 했을 때, 성공하는 경우가 있었다.

레이의 Distance를 1로 설정했는데도 불구하고 성공하는 경우가 있다는 건 길이의 문제가 아니라고 판단했고, Raycast의 문제라고 생각했다.

따라서 저런 질문글을 올렸다.

이어지는 실패에 두 상호작용 테스트 중 하나를 주석처리하고 돌리는 경우 성공하는 경우가 매우 높아졌는데, 사실 이 경우는 해결하고 보니 명확했지만 당시에는 매우 헷갈리게 하는 일종의 rabbit hole(뭘 모르는지 모르는 문제)이였다.

사실 몇 가지 실패가 나온 이유는 Movement의 영향을 받아 x축으로 1만큼 이동한 뒤 상호작용을 하기 때문에 상호작용이 성공하는 확률이 있기 때문이다.

이로써 테스트 코드는 순서대로 실행되는 것이 아니라는 것을 알게 되었다. (Nunit의 경우 Order라는 속성을 통해 순서를 정할 수 있지만, 유니티의 경우 일부 버전만 지원한다.)

저 사실을 몰랐기 때문에 성공과 실패가 자꾸 왔다 갔다 하니 이해가 되지 않았던 것이다.

학습의 시점으로 본다면 피드백이 명확하지 않았던 것 (스키너의 상자처럼)

유니티 테스트 코드 관련 자료는 인터넷에 정말 한정되어 있기 때문에 더욱 힘들었다.

직접 테스트 코드를 디버깅하면서 되게 많은 생각이 들었는데 해당 내용은 마지막 느낀점이 적혀있다.

돌아와서 테스트 코드를 디버깅 하거나, 스레드, GC를 직접 보면서까지 알아봤지만 해결되지 않았는데..

문제는 생각보다 쉽게 해결되었다.

해결

과거 Edit Mode에서 작성한 테스트 코드를 Play Mode로 직접 옮기면서 간과한 부분이 있었는데, 그걸 그대로 빼먹고 공식문서를 읽다가 발견한 것..

원래는 PlayMode에서 테스트를 하게 되면 일정 시간이 더 걸려서 EditMode에서 테스트를 진행했는데, 인게임 관련 테스트 코드를 명확하게 분리하는 게 좋다고 하여 PlayMode로 옮겼다.

옮기면서 [Test]가 아닌 [UnityTest]로 바꿔주었는데, 프레임 반환 방식이 달랐단 것을 몰랐던 것이다. (공식문서를 꼼꼼하게 읽자!)

따라서 테스트 코드에서 생성한 오브젝트가 삭제되지 않고 다른 상호작용 테스트에 영향을 주었고, 실행순서도 정해져 있지 않았기에 실패와 성공이 왔다갔다 했던 것이다.

따라서 프레임을 넘겨주어 (이벤트 함수나, 오브젝트 생성/삭제 등)을 통해 런타임 테스트 코드를 작성해야 한다.

단위 테스트보다 좀 더 복잡한, 컴포넌트 테스트정도로 생각하면 될 것 같고, 유니티 특성상 PlayMode라는 특별한 테스트 장치를 제공하기 때문에 이를 활용하는 것이 좋다.

실제로 유니티 PlayMode테스트는 실제 해쉬값으로 이름을 가진 임시 씬을 생성하여 해당 씬에서 테스트를 진행한다.

느낀점

뭔가 테스트 코드를 테스트 코드하는 느낌이였다.

무한루프 처럼..

이게 과연 테스트 코드를 작성하는 의미가 있을까? 라는 생각도 들고.. (물론 멀리본다면 좋은 효과가 있다고 확신한다.)

이렇게 오히려 성공과 실패가 비 논리적으로 나오는 상황이 몇배는 더 어렵다는 것도 배운 것 같다.

테스트 코드를 짜기 위해 코드를 작성함으로 객체지향적으로 짜게 되는 것은 좋지만, 유니티 자체로도 하나의 툴이기 때문에 유니티쪽에 더 가까워 지는게 더 현명할 수 있겠다는 생각도 많이 들었던 것 같다.

정리하자면 좋은 경험이고, 생각자체도 많이 할 수 있어서 좋았다..!

댓글남기기