주사위를 굴리는 사용 사례를 다루는 좋은 단위 테스트는 무엇입니까?


18

단위 테스트를 시작하려고합니다.

기본면의 수는 6과 같지만 4, 5면 등이 될 수있는 다이가 있다고 가정 해 봅시다.

import random
class Die():
    def __init__(self, sides=6):
        self._sides = sides

    def roll(self):
        return random.randint(1, self._sides)

다음은 유효 / 유용한 단위 테스트입니까?

  • 6면 다이에 대해 1-6 범위의 롤 테스트
  • 6면 다이에 대해 롤 0을 테스트
  • 6면 다이에 대해 7 롤 테스트
  • 3면 다이에 대해 1-3 범위의 롤 테스트
  • 3면 다이에 대해 롤 0을 테스트
  • 3면 다이의 롤 4 개를 테스트

무작위 모듈이 오랫동안 오랫동안 사용되어 왔기 때문에 이것들이 시간 낭비라고 생각하지만 무작위 모듈이 업데이트되면 (예를 들어, 파이썬 버전을 업데이트하는 경우) 최소한 커버됩니다.

또한이 경우 3과 같은 다른 다이 롤 변형을 테스트해야합니까? 아니면 초기화 된 다른 다이 상태를 포함하는 것이 좋습니까?


1
마이너스 5면 다이 또는 널면 다이는 어떻습니까?
JensG

답변:


22

테스트에서 random모듈이 작업을 수행하고 있는지 확인해서는 안됩니다 . unittest는 다른 코드와 상호 작용하는 방식이 아니라 클래스 자체 만 테스트해야합니다 (별도로 테스트해야 함).

물론 코드가 random.randint()잘못 사용하는 것은 전적으로 가능합니다 . 또는 당신은 random.randrange(1, self._sides)대신 전화 를하고 당신의 주사위는 결코 가장 높은 가치를 던지지 않지만 그것은 다른 종류의 버그 일 것입니다. 단일 테스트로 잡을 수는 없습니다. 이 경우 die 장치 가 설계된대로 작동하지만 디자인 자체에 결함이 있습니다.

이 경우에는 조롱을 사용 하여 함수 를 바꾸고 올바르게 호출 되었는지 randint()확인 합니다. Python 3.3 이상에는 이러한 유형의 테스트를 처리 하는 모듈 이 제공 되지만 이전 버전에 외부 패키지 를 설치하여 동일한 기능을 얻을 수 있습니다unittest.mockmock

import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()

조롱하면 테스트가 매우 간단 해집니다. 실제로 2 가지 경우가 있습니다. 6면 금형의 기본 경우와 사용자 정의 측면 경우입니다.

randint()의 전역 네임 스페이스에서 함수 를 임시로 대체하는 다른 방법이 Die있지만 mock모듈이이를 가장 쉽게 만듭니다. 여기서 @mock.patch데코레이터 는 테스트 케이스의 모든 테스트 방법에 적용됩니다 . 각 테스트 메소드 random.randint()에는 모의 함수 라는 추가 인수가 전달 되므로 모의 테스트를 통해 모의 함수가 제대로 호출되었는지 확인할 수 있습니다. return_value가 호출 될 때 우리가 있음을 확인할 수 있도록, 모의에서 반환되는 것을 인수 지정 die.roll()방법은 참으로 우리에게 '랜덤'결과를 반환했습니다.

여기에 또 다른 Python unittesting 모범 사례를 사용했습니다 : 테스트의 일부로 테스트중인 클래스를 가져옵니다. 이 _make_one메소드는 가져 오기 및 인스턴스화 가 test 내에서 작동 하므로 구문 오류 또는 원래 모듈을 가져 오지 못하게하는 다른 실수가 있어도 테스트 모듈 이 계속로드됩니다.

이런 식으로 모듈 코드 자체에서 실수를하더라도 테스트는 계속 실행됩니다. 코드의 오류에 대해 알려 주면 실패합니다.

분명히, 위의 테스트는 극단적으로 간단합니다. 여기서 목표는 random.randint()예를 들어 올바른 인수로 호출 된 테스트를하는 것이 아닙니다 . 그 대신 목표는 특정 입력에 따라 장치가 올바른 결과를 생성하는지 테스트하는 것입니다.이 입력에는 테스트 되지 않은 다른 장치의 결과가 포함됩니다 . random.randint()메서드를 조롱함으로써 코드에 대한 다른 입력 만 제어 할 수 있습니다.

실제 테스트 에서는 테스트 대상 단위의 실제 코드가 더 복잡해집니다. API에 전달 된 입력과의 관계 및 다른 단위가 호출되는 방식은 여전히 ​​흥미로울 수 있으며 조롱하면 중간 결과에 액세스 할 수있을뿐만 아니라 해당 호출의 반환 값을 설정할 수 있습니다.

예를 들어, 타사 OAuth2 서비스 (다단계 상호 작용)에 대해 사용자를 인증하는 코드에서 코드가 올바른 데이터를 해당 타사 서비스로 전달하고 있는지 테스트하고, 타사 서비스가 반환되므로 전체 OAuth2 서버를 직접 구축하지 않고도 다양한 시나리오를 시뮬레이션 할 수 있습니다. 여기서 첫 번째 응답의 정보가 올바르게 처리되었고 두 번째 단계 호출로 전달되었는지 테스트하는 것이 중요하므로 모의 서비스가 올바르게 호출되고 있는지 확인하고 싶습니다.


1
2 가지 이상의 테스트 사례가 있습니다 ... 결과 기본값 확인 : 하한 (1), 상한 (6), 하한 (0), 상한 (7) 및 max_int와 같은 사용자 지정 숫자에 대한 결과 입력도 검증되지 않아 어느 시점에서 테스트해야 할 수도 있습니다.
James Snell

2
아니요, randint()의 코드가 아닌에 대한 테스트입니다 Die.roll().
Martijn Pieters

실제로 randint가 올바르게 호출되는 것이 아니라 결과도 올바르게 사용되도록하는 방법이 있습니다. sentinel.die예를 들어 (sentinel 객체 unittest.mock도 마찬가지입니다) 그런 다음 roll 메소드에서 반환 된 것이 맞는지 확인하십시오. 실제로 테스트 된 메소드를 구현하는 한 가지 방법 만 허용합니다.
aragaer

@aragaer : 값이 변경되지 않은 상태로 리턴되는지 확인하려면이를 확인 sentinel.die하는 좋은 방법입니다.
Martijn Pieters

특정 값으로 mocked_randint가 호출되도록하려는 이유를 이해하지 못합니다. randint를 조롱하여 예측 가능한 값을 반환하려는 것을 알고 있지만 호출 된 값이 아니라 예측 가능한 값을 반환한다는 우려가 아닙니다. 호출 된 값을 확인하는 것은 불필요하게 테스트를 구현의 세부 사항에 불필요하게 묶는 것 같습니다. 또한 왜 우리는 randint의 정확한 값을 주사위가 반환하도록주의해야합니까? 실제로 1보다 큰 값과 최대 값보다 작은 값을 반환하도록 신경 쓰지 않습니까?
bdrx

16

Martijn의 답변 은 random.randint를 호출하고 있음을 보여주는 테스트를 실제로 실행하려는 경우 어떻게 수행하는지입니다. 그러나 "질문에 답이 없다"는 말을 할 위험이 있으므로, 단위 테스트를하지 않아야한다고 생각합니다. randint를 모의하는 것은 더 이상 블랙 박스 테스트가 아닙니다 . 구현 에서 특정 일이 진행되고 있음을 구체적으로 보여줍니다 . 이 결과는 것을 증명하는 것을 실행할 수있는 테스트가없는 -을 테스트 블랙 박스도 옵션이 아닙니다 결코 미만 1 또는 6 개 이상을 할 수 없습니다가.

당신은 조롱 할 수 있습니까 randint? 그래 넌 할수있어. 그러나 당신은 무엇을 증명하고 있습니까? 인수 1과 측면으로 호출했습니다. 무엇합니까 의미? 원점에서 넌 다시 - - 당신이 입증 할 필요하게 될 겁니다 하루의 끝에 공식적 또는 비공식적으로 - 전화가 random.randint(1, sides)제대로 주사위 롤을 구현합니다.

나는 단위 테스트를 위해 모두입니다. 그들은 환상적인 위생 검사이며 버그의 존재를 드러냅니다. 그러나 그들은 그들의 부재를 증명할 수 없으며 테스트를 통해 주장 할 수없는 것들이 있습니다 (예 : 특정 함수는 결코 예외를 던지거나 항상 종료되지 않습니다).이 특별한 경우, 나는 당신이 용납 할 것이 거의 없다고 생각합니다. 이득. 결정론적인 행동의 경우, 단위 테스트는 예상되는 답변이 무엇인지 실제로 알고 있기 때문에 의미가 있습니다.


단위 테스트는 실제로 블랙 박스 테스트가 아닙니다. 다양한 부품이 설계된대로 상호 작용한다는 것을 확인하기 위해 통합 테스트가 필요한 것입니다. 물론, (대부분의 테스트 철학은) 의견의 문제입니다 . "단위 테스트"가 화이트 박스 또는 블랙 박스 테스트에 해당합니까?를 참조하십시오. 블랙 박스 단위 테스트 일부 (스택 오버플로) 관점합니다.
Martijn Pieters 2016 년

@MartijnPieters 나는 이것이 "통합 테스트의 목적"이라고 동의하지 않는다. 통합 테스트는 시스템의 모든 구성 요소가 올바르게 상호 작용하는지 확인하기위한 것입니다. 주어진 구성 요소가 주어진 입력에 대해 올바른 출력을 제공하는지 테스트 할 장소가 아닙니다. 블랙 박스 대 화이트 박스 단위 테스트의 경우 화이트 박스 단위 테스트는 구현 변경으로 인해 결국 중단되며 구현에서 가정 한 모든 가정이 테스트로 이어질 수 있습니다. 그것이 잘못 된 경우 random.randint호출 된 유효성 검사 1, sides는 가치가 없습니다.
Doval

예, 이것이 화이트 박스 단위 테스트의 한계입니다. 그러나 테스트 에서 [1, sides (포함)] 범위의 값을 올바르게 반환 하는 지점없습니다 . 단위가 올바르게 작동하는지 random.randint()확인하는 것은 Python 개발자의 몫 random입니다.
Martijn Pieters 2016 년

그리고 스스로 말했듯이, 단위 테스트는 코드에 버그가 없음을 보장 할 수 없습니다. 코드가 잘못 (말을 다른 단위를 사용하는 경우, 당신은 예상 random.randint()처럼 행동하기 random.randrange()로 호출하여하고 random.randint(1, sides + 1). 그럼 당신은 어쨌든 침몰하는,
마티 피에 터스

2
@MartijnPieters 나는 당신에게 동의하지만, 그것은 내가 반대하는 것이 아닙니다. random.randint가 arguments (1, side)로 호출되고 있는지 테스트 하는 데 반대합니다 . 구현에서 이것이 옳은 일이라고 가정했으며 이제 테스트에서 그 가정을 반복하고 있습니다. 그 가정이 틀리면 테스트는 통과했지만 구현은 여전히 ​​올바르지 않습니다. 그것은 작성하고 유지하기에 완전한 고통이라는 반 암기 증거입니다.
Doval

6

랜덤 시드 수정. 1, 2, 5 및 12면 주사위의 경우 수천 롤이 1과 N을 포함하여 0 또는 N + 1을 포함하지 않는 결과를 제공하는지 확인하십시오. 예상 범위를 커버하고 다른 시드로 전환하십시오.

조롱 도구는 멋지지만 작업을 수행 할 수 있다고해서 일을해야한다는 의미는 아닙니다. YAGNI는 기능만큼 테스트 픽스처에도 적용됩니다.

비 조종 종속성으로 쉽게 테스트 할 수 있다면 거의 항상해야합니다. 이렇게하면 테스트 횟수가 증가하는 것이 아니라 결함 개수를 줄이는 데 집중할 수 있습니다. 과도한 조롱으로 오해의 소지가있는 커버리지 수치가 생성되어 실제 테스트를 이후 단계로 연기 할 수 있습니다.


3

당신이 Die그것에 대해 생각하면 무엇입니까 ? -랩퍼 이상 random. random.randint응용 프로그램 자체 어휘 측면에서 캡슐화 하고 레이블을 다시 지정합니다 Die.Roll.

나는 사이에 추상화 계층을 삽입하는 것이 관련 찾을 수없는 Dierandom때문에 Die그 자체가 이미 간접의 계층입니다 응용 프로그램 및 플랫폼 사이.

통조림 주사위 결과를 원한다면, 조롱 Die하고 조롱 하지 마십시오random .

일반적으로 외부 시스템과 통신하는 래퍼 객체를 단위 테스트하지 않고 통합 테스트를 작성합니다. 몇 가지를 쓸 수는 Die있지만 기본 객체의 임의의 특성으로 인해 의미가 없습니다. 또한 여기에는 구성 또는 네트워크 통신이 없으므로 플랫폼 호출 외에는 테스트 할 것이 많지 않습니다.

=> Die이 코드는 몇 줄의 간단한 코드 일 뿐이고 random그 자체에 비해 로직이 거의 없거나 전혀 없다는 점을 고려하면 해당 예제에서 테스트를 건너 뜁니다.


2

내가 볼 수있는 한, 난수 생성기를 시드하고 예상 결과를 확인하는 것은 유효한 테스트가 아닙니다. 그것은 주사위가 내부적으로 어떻게 작동하는지에 대한 가정을합니다. 파이썬 개발자는 난수 생성기 또는 다이를 변경할 수 있습니다 (참고 : "주사위"는 복수형이고 "다이"는 단수입니다. 한 번의 호출로 여러 다이 롤을 구현하지 않는 한 "다이"라고 할 수 있습니다) 다른 난수 생성기를 사용하십시오.

마찬가지로 임의 함수를 조롱하면 클래스 구현이 예상대로 정확하게 작동한다고 가정합니다. 왜 그렇지 않습니까? 누군가가 기본 파이썬 난수 생성기를 제어 할 수 있으며,이를 방지하기 위해 향후 버전의 주사위는 더 많은 난수 데이터를 혼합하기 위해 여러 난수 또는 더 큰 난수를 가져올 수 있습니다. NSA가 CPU에 내장 된 하드웨어 난수 생성기를 조작하고 있다고 의심했을 때 FreeBSD 운영 체제 제조업체도 비슷한 방식을 사용했습니다.

그것이 나이라면 6000 롤을 돌리고 집계하고 1-6의 각 숫자가 500과 1500 사이에서 롤링되는지 확인하십시오. 또한 해당 범위 밖의 숫자가 반환되지 않는지 확인합니다. 또한 두 번째 6000 롤 세트의 경우 주파수 순서대로 [1..6]을 주문할 때 결과가 다른지 확인합니다 (숫자가 임의 인 경우 720 회 실행 중 한 번 실패 함). 철저하고 싶다면 1 뒤에, 2 뒤에 오는 숫자의 빈도를 찾을 수 있습니다. 그러나 표본 크기가 충분히 크고 분산이 충분한 지 확인하십시오. 인간은 임의의 숫자가 실제보다 적은 패턴을 가질 것으로 기대합니다.

12면 및 2면 다이에 대해 반복하십시오 (6이 가장 많이 사용되므로이 코드를 작성하는 모든 사람에게 가장 기대됩니다).

마지막으로 단면 다이, 0면 다이, -1면 다이, 2.3면 다이, [1,2,3,4,5,6]면 다이에서 어떤 일이 발생하는지 테스트합니다. "블라"면의 주사위. 물론 이것들은 모두 실패해야한다. 유용한 방법으로 실패합니까? 롤링이 아닌 생성시 실패 할 수 있습니다.

또는, 당신은 아마도 이것들을 다르게 처리하기를 원할 것입니다. 아마도 [1,2,3,4,5,6]으로 주사위를 만드는 것은 받아 들일 수 있어야합니다. 이것은 4 개의면이 있고 각면에 글자가있는 주사위 일 수 있습니다. 마술 "8 공"과 마찬가지로 게임 "Boggle"이 떠 오릅니다.

그리고 마지막으로 이것을 고려하고 싶을 수도 있습니다 : http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg


2

조류에 대항하여 수영 할 위험이 있기 때문에, 나는 수년 전까지 지금까지 언급되지 않은 방법을 사용하여이 정확한 문제를 해결했습니다.

내 전략은 단순히 전체 공간에 걸친 예측 가능한 값 스트림을 생성하는 RNG를 조롱하는 것이 었습니다. (예) side = 6이고 RNG가 0에서 5까지의 값을 순서대로 생성하면 클래스가 어떻게 동작하는지 예측하고 그에 따라 단위 테스트를 수행 할 수 있습니다.

이론적 근거는 이것이 RNG 자체를 테스트하지 않고 RNG가 결국 각 값을 생성 할 것이라는 가정하에이 클래스의 논리 만 테스트한다는 것입니다.

간단하고 결정적이며 재현 가능하며 버그를 잡습니다. 나는 같은 전략을 다시 사용할 것이다.


이 질문은 RNG가 존재하는 경우 테스트가 무엇인지, 테스트에 어떤 데이터가 사용 될지에 대해 설명하지 않습니다. 내 제안은 RNG를 조롱하여 철저히 테스트하는 것입니다. 테스트 할 가치가있는 질문은 질문에없는 정보에 달려 있습니다.


예측 가능하도록 RNG를 조롱한다고 가정하십시오. 그럼 당신은 무엇을 테스트합니까? 질문은 "다음은 유효 / 유용한 단위 테스트입니까?" 0-5를 반환하도록 테스트하는 것은 테스트가 아니라 테스트 설정입니다. "그에 따라 단위 테스트"는 어떻습니까? 나는 그것이 "버그를 잡는 방법"을 이해하지 못한다. 나는 '단위'테스트에 필요한 것을 이해하는 데 어려움을 겪고 있습니다.
bdrx

@bdrx : 이것은 얼마 전입니다. 나는 지금 다르게 대답 할 것입니다. 그러나 편집을 참조하십시오.
david.pfx

1

귀하의 질문에 제안한 테스트는 모듈 식 산술 카운터를 구현으로 감지하지 않습니다. 그리고 그들은 확률 분포 관련 코드에서와 같은 일반적인 구현 오류를 감지하지 못합니다 return 1 + (random.randint(1,maxint) % sides). 또는 2 차원 패턴을 생성하는 생성기 변경.

균등하게 분포 된 임의의 숫자를 생성하는지 실제로 확인하려면 매우 다양한 속성을 확인해야합니다. 합리적으로 좋은 일 을하기 위해 생성 된 숫자에 http://www.phy.duke.edu/~rgb/General/dieharder.php 를 실행할 수 있습니다 . 또는 유사하게 복잡한 단위 테스트 스위트를 작성하십시오.

그것은 단위 테스트 또는 TDD의 결함이 아니며 무작위성은 검증하기 매우 어려운 속성입니다. 예를 들어 인기있는 주제입니다.


-1

다이 롤의 가장 쉬운 테스트는 단순히 수백 번 반복하고 가능한 각 결과가 대략 (1 /면 수) 번 맞는지 확인하는 것입니다. 6면 주사위의 경우 가능한 각 값이 시간의 약 16.6 %에 도달 한 것을 볼 수 있습니다. 퍼센트 이상이 꺼져 있으면 문제가있는 것입니다.

이 방법을 사용하면 테스트를 변경하지 않고 난수를 생성하는 기본 메커니즘을 쉽고, 가장 중요하게 리팩터링 할 수 있습니다.


1
이 테스트는 사전에 정의 된 순서대로 한
면씩

1
코더가 악의로 무언가를 구현하려는 의도 (다이에서 무작위 에이전트를 사용하지 않음)를 단순히 '빨간색 표시등이 녹색으로 변하게'하려는 것을 찾으려고하면 단위 테스트가 실제로 해결할 수있는 것보다 더 많은 문제가 있습니다.
ChristopherBrown
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.