본문 바로가기
카테고리 없음

일부 테스트의 냄새를 맡자 #3 — Java에서 무작위성 테스트하는 방법

by 부동산절대원칙 2022. 6. 23.
반응형

일부 테스트의 냄새를 맡자 #3 — Java에서 무작위성 테스트하는 방법

 

비결정적 행동 테스트를 피해야 하는 이유

Unsplash에서 Lucas George Wendt의 사진

안녕하세요. 네 번째 기사에 오신 것을 환영합니다. “몇 가지 테스트의 냄새를 맡자” 시리즈. 이전 에피소드¹에서 내부 동작을 실행하는 테스트를 살펴보고 이 접근 방식의 단점을 지적했습니다.

오늘 우리는 또 다른 흥미로운 주제를 분석할 것입니다. 테스트할 수 없는 테스트가 나쁜 습관인 이유와 테스트 케이스가 지나치게 복잡해지는 이유입니다.

다음은 오늘의 예에서 사용하는 것입니다.

  • 자바 17
  • JUnit 5.8.2
  • AssertJ 3.22.0
  • 목토 4.5.1

요구 사항

이 사용 사례에 대한 요구 사항은 다음과 같습니다.

  • 게임이 시작되면 지도에 새 플레이어를 생성합니다.
  • 지도는 타일의 행렬입니다. 지도의 크기는 구성할 수 있습니다
  • 기본적으로 플레이어를 임의의 타일에 배치합니다.

구현에 대한 첫 번째 접근 방식

이러한 요구 사항의 샘플 구현은 아래 스니펫과 같습니다.

테스트를 통과했지만 다시 작업해야 합니다.

짧은 설명:

  • 그만큼 Board 클래스는 객체의 행렬로 구성되며 각 객체는 다음과 같습니다. Tile 유형
  • 그만큼 Tile 클래스는 x 그리고 y 속성에 추가하여 Localizable 플레이어와 같은 개체; 미래에 Localizable 인터페이스는 또한 적, 소모품 및 지도의 타일에 묶일 수 있는 모든 것과 같은 다른 개체를 나타낼 수 있습니다.
  • 지도 어딘가에 새 플레이어를 배치하고 싶기 때문에 Player 클래스 구현 Localizable 함께 제공되는 인터페이스getCurrentLocation() 기능.
  • 새로 인스턴스화하려면 Player팩토리 메소드를 호출해야 합니다. create(Board board)
  • 요구 사항을 충족하기 위해, Player 클래스에는 플레이어가 게임을 시작해야 하는 임의의 타일을 선택하는 논리가 포함되어 있습니다.

코드는 제대로 작동하거나 작동하고 고품질을 유지하도록 작성될 수 있습니다. 현재 구현은 불행히도 첫 번째 옵션을 나타냅니다.

위의 코드가 컴파일되고 테스트가 통과하고 이론적으로 다른 기능을 구현할 수도 있지만 더 잘할 수 있는 부분을 살펴보겠습니다.

  • 테스트 케이스 SpawnPlayerTest#testSpawnPlayerOnTheMap 보드 크기를 확인하여 너무 많이 확인합니다(테스트 이름에는 플레이어 스폰에만 초점을 맞추는 것으로 나와 있으며 이는 사실이 아닙니다)
  • 주장 논리가 너무 복잡합니다(플레이어가 할당된 타일은 무작위이기 때문에 우리는 보드에서 모든 타일을 수집하고 플레이어가 그 중 하나에 할당되었는지 확인하고 있습니다)
  • 새 플레이어의 타일을 선택하는 논리는 Player (단일 책임 및 개방형 원칙 위반)

무작위로 작업하는 것은 까다로울 수 있으며 이것이 우리 코드의 경우입니다. 이 예에 따르면 크기가 5x4 타일인 보드가 있습니다. 이것은 우리에게 스폰 플레이어를 위한 타일을 선택할 수 있는 20개의 가능한 타일을 제공합니다.

이제 말할 중요한 사항이 있습니다. 주장할 수 있는 값이 20개 있다는 것을 알고 있더라도 다음 테스트 실행에서 어떤 특정 타일이 선택될지 확신할 수 있는 방법은 사실상 없습니다. 무작위성은 우리가 통제할 수 없는 부분이며, 제 생각에는 테스트에서 이에 맞서 싸우는 것보다 이 사실을 받아들이는 것이 더 나을 것입니다.

현재 요구 사항에 더 잘 맞을 수 있는 솔루션은 몇 가지 사항으로 설명할 수 있습니다.

  • 플레이어의 타일을 별도의 유닛으로 선택하는 로직 추출
  • 요구 사항이 변경되는 경우 다른 구현을 사용할 수 있도록 이 논리에 대한 추상화를 만듭니다.
  • 플레이어를 스폰하려면 타일 선택기를 Player 생성자를 통한 클래스
  • 테스트 단순화 SpawnPlayerTest#testSpawnPlayerOnTheMap

1단계: 플레이어의 타일 선택 추출

미래에 요구 사항이 변경되면 또한 구현하는 다른 클래스를 작성해야 합니다. LocationSeed 상호 작용.

2단계: 플레이어 개체에게 초기 타일을 가져올 위치를 알려줍니다.

생성자는 제공된 LocationSeed 를 호출하여 객체 locationSeed.getTile() 방법. 미래에는 단순히 다른 구현을 제공할 수 있습니다. LocationSeed 수정을 위해 닫혀 있지만 확장을 위해 열려 있도록 필요할 때 인터페이스를 제공합니다.

3단계: 테스트 재작업

새로운 테스트 클래스에서 별도의 테스트 메소드로 보드 생성 로직을 추출했습니다. 이제 이 테스트는 그 이름이 시사하는 바를 정확히 확인합니다.

이제부터 기존 테스트가 간소화됩니다. 플레이어를 스폰하는 데만 초점을 맞춥니다.

나는 사용했다 mock() 가짜 구현을 만드는 Mockito의 메소드 RandomLocationSeed 수업. 우리는 테스트에서 플레이어 생성이 예상대로 작동하는지 확인하기를 원했기 때문에 처음에는 다소 직관적이지 않은 것처럼 보일 수 있습니다.

"지도의 임의의 장소에 플레이어를 생성"하는 관찰 가능한 동작을 테스트해야 함을 기억하십시오. 그러나 무작위성은 우리가 제어할 수 없는 외부 요인이기 때문에 제어할 수 있는 종속성을 사용하여 이 테스트를 설정하려고 합니다.

그런 다음 호출하여 모의를 설정했습니다. when() 그리고 then() Mockito에서도 제공하는 기능입니다. 이 함수는 메서드가 호출될 때 수행해야 하는 작업을 구성하는 데 사용됩니다. 나는 단순히 질문했다 Board 개체가 타일 중 하나를 반환할 때마다 getTile() 함수가 호출되면 샘플 타일을 반환합니다.

조롱과 관련하여 마지막으로 LocationSeed 전화하고있다 verify() 모의가 전혀 호출되었는지 여부를 확인하는 메서드입니다.

이 시점에서 위의 테스트를 마지막으로 약간 수정하는 것이 좋습니다. 사실, 우리는 모의(mock)가 아닌 스텁(stub)으로 테스트를 다루고 있습니다. 그들 사이의 주요 차이점은 모의 객체는 값을 반환하지 않는 반면(명령 역할을 함) 스텁은 값을 반환합니다(쿼리 역할을 함).

전화 걸기 verify() 스텁의 메서드는 내부 동작을 확인하기 때문에 나쁜 습관입니다. 우리의 경우 관찰 가능한 동작은 "플레이어 생성"이므로 타일을 쿼리하는 것은 비즈니스 사례의 중간 단계일 뿐입니다. 따라서 호출을 삭제합시다. verify() 테스트의 최종 버전이 다음과 같이 보이도록 합니다.

모의 호출 여부를 확인하지 않은 테스트의 최종 버전
테스트의 최종 버전이 통과되었습니다.

보시다시피, 프로그래밍 게임은 게임을 하는 것만큼 재미있을 수 있습니다. :).

이 예에서 얻은 교훈을 요약해 보겠습니다.

  • 테스트를 지나치게 복잡하게 만들지 마십시오
  • 테스트가 너무 많은 작업을 수행하는 경우, 즉 여러 assert 명령문을 작성하고 여러 객체를 확인하려면 이 테스트를 더 작은 것으로 분할하는 것이 좋습니다.
  • 모든 기능이 직접 테스트에 적합한 것은 아닙니다(예: 임의성에 따른 기능).
  • 상황이 통제할 수 없는 곳에서 모의/스텁을 사용합니다(임의성을 테스트하지만 파일 시스템, 웹 서비스/메시지 버스에 대한 호출 등과 같은 애플리케이션 경계를 넘기도 함).

[1]: 크리스티안 슈피차코프스키, 몇 가지 테스트의 냄새를 맡자 #2 https://betterprogramming.pub/lets-smell-some-tests-2-asserting-the-internal-behavior-in-java-1c0f34fe8bbc

반응형