-
파이썬 TDD 예제: 숫자야구게임 만들기 #1 (feat. unittest)Programing/TDD 2023. 1. 28. 02:03반응형
TDD 연습으로 숫자야구게임 만들기를 많이 하길래,
저희 사내스터디에서도 책갈이를 한 이후에 숫자야구게임을 각자 만들어보기로 했습니다.
만들면서 느꼈던 점, 서로 피드백을 하면서 추가로 느낀 점들이 있었는데,
이 내용들은 각각 중간중간에 서술하도록 하겠습니다.
파이썬으로 TDD를 진행하는 기본적인 방식에 대해서는 아래의 글을 참조해 주세요
숫자게임프로그램 전제 규칙, 고려할 요건
[숫자야구 게임규칙] - com: 랜덤한 세자리 숫자로 된 문제 생성 - user: 문제의 숫자를 맞추기 com - 0~9 숫자를 중복 없이 사용해 3자리 숫자를 생성 - 유저가 숫자를 예상할 때마다 규칙대로 out, ball, strike 여부를 도출 규칙 - 정답에 사용한 숫자가 맞지만 위치가 다르다면 ball - 예) 정답:369 / 유저:123 -> 1B - 위치까지 같다면 strike - 예) 정답:369 / 유저:789 -> 1S - 유저가 예상한 숫자 중 어떠한 3자리 숫자 중 어떠한 숫자도 정답 숫자에 사용하지 않았다면 out - 예) 정답:369 / 유저:248 -> OUT [고려할 요건, 테스트] 0. 게임은 매번 규칙에 맞는 랜덤한 숫자를 생성해야함 1. 유저의 예측이 정답인 경우 - 3S - 3S임을 알려 준 뒤 게임종료 2. 유저의 예측 숫자에 사용된 3개 숫자 중 어떠한 숫자도 정답에 사용되지 않은 경우 -> 완료 - out 3. 예측 숫자 중 스트라이크가 있는 경우 - 스트라이크의 개수에 따라 1S, 2S 출력 4. 예측 숫자 중 볼이 있는 경우 - 볼의 개수에 따라 1B, 2B 출력 5. 스트라이크와 볼이 섞여있는 경우 - 스트라이크, 볼의 개수에 따라 1S 1B, 1S 2B 6. 한번만 결과를 알려주고 끝나는게 아니라, 유저와 지속적으로 소통을 해야함 - 유저가 정답을 맞출 때 까지 7. 정답을 맞추면 맞췄다고 알려주고 게임 종료 8. 예측 숫자가 3자리 수가 아닌 경우 9. 예측 숫자 중 중복이 있는 경우
만들면서 생각난 것
- 스터디 도서에 따르면 예외 사항부터 만드는 것이 좋은데, 정작 그 예외 사항이 처음엔 생각이 잘 안나는 경우가 많습니다.
- 위의 8, 9가 그렇습니다.
- 거진 다 만들고 나서야 생각나서 추후에 추가하였습니다.
피드백 하면서 생각난 것
- 결국 혼자서는 생각을 못할 때도 많습니다.
- "숫자가 아닌 문자가 입력되는 경우"라는 간단한 예외사항이 왜 생각나지 않았었나 모르겠습니다.
첫번째 테스트: 정답인 경우 3S (+리펙토링)
(제가 리펙토링 하기 이전 과정도 -구태여-보고 싶으시다면 더보기를 눌러주세요)
더보기1) 정답인 경우 S3
NumberBaseball_test.py
import unittest from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): game = Game() def test_guess_correct_answer_then_3S(self): guess = "369" result = self.game.guess_checker(guess) self.assertEqual("3S", result)
NumberBaseball.py
import random class Game: def guess_checker(self, guess): if guess == "369": return "3S"
2) init 추가 -> 다양한 정답 테스트 가능하도록
NumberBaseball_test.py
import unittest from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): def test_guess_correct_answer_then_3S(self): game = Game("369") result = game.guess_checker("369") self.assertEqual("3S", result) game1 = Game("106") result1 = game1.guess_checker("106") self.assertEqual("3S", result1)
NumberBaseball.py
import random class Game: def __init__(self, correct_answer): if correct_answer: self.correct_anser = correct_answer else: self.correct_anser = random.sample(['0','1','2','3','4','5','6','7','8','9'],3) self.correct_anser = ''.join(self.correct_anser) def guess_checker(self, guess): if guess == self.correct_anser: return "3S" else: return f"test용 print : {self.correct_anser}"
NumberBaseball_test.py
import unittest from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): def assertGuessResult(self, correct_answer, guess_answer, right_chk_result): game = Game(correct_answer) game_chk_result = game.guess_checker(guess_answer) self.assertEqual(right_chk_result, game_chk_result) def test_guess_correct_answer_then_3S(self): self.assertGuessResult("369", "369", "3S") self.assertGuessResult("106", "106", "3S")
NumberBaseball.py
import random class Game: def __init__(self, correct_answer): if correct_answer: self.correct_anser = correct_answer else: self.correct_anser = random.sample(['0','1','2','3','4','5','6','7','8','9'],3) self.correct_anser = ''.join(self.correct_anser) def guess_checker(self, guess): if guess == self.correct_anser: return "3S" else: return f"test용 print : {self.correct_anser}"
만들면서 생각난 것
- 숫자야구 프로그램은 게임 시작시 정답숫자를 랜덤으로 생성/배정합니다.
- 때문에 테스트에 어려움이 있으므로 대역을 활용해야 할 것 같았습니다.
- 그런데 저렇게 코드를 작성하고 나서는 어떻게 해야 대역을 활용할 수 있을지 전혀 모르겠더랍니다.
피드백 하면서 생각난 것
- 다른분들의 코드를 보면서 제 코드에서 왜 대역을 사용하지 못했나 깨달았습니다.
- 제 코드에서는 init 안에 self.correct_answer를 바로 생성해버리는데,
다른분들은 따로 def number_generator 와 같은 형태로 메소드를 이용하시더라구요.- 이렇게 하면 대역을 사용하는게 가능해집니다.
- (+ answer 오타났다)
두번째 테스트 : 완전 오답인 경우 OUT
NumberBaseball_test.py
import unittest from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): def assertGuessResult(self, correct_answer, guess_answer, right_chk_result): game = Game(correct_answer) game_chk_result = game.guess_checker(guess_answer) self.assertEqual(right_chk_result, game_chk_result) def test_guess_correct_answer_then_3S(self): self.assertGuessResult("369", "369", "3S") self.assertGuessResult("106", "106", "3S") def test_guess_all_wrong_answer_then_OUT(self): self.assertGuessResult("369", "145", "OUT") self.assertGuessResult("106", "275", "OUT")
NumberBaseball.py
import random class Game: def __init__(self, correct_answer): if correct_answer: self.correct_anser = correct_answer else: self.correct_anser = random.sample(['0','1','2','3','4','5','6','7','8','9'],3) self.correct_anser = ''.join(self.correct_anser) def guess_checker(self, guess): if guess == self.correct_anser: return "3S" # out check out_checker = 0 for n in guess: if n not in self.correct_anser: out_checker += 1 if out_checker == 3: return "OUT" else: return f"test용 print : {self.correct_anser}"
세번째 테스트 : 자리 일치하는 경우 Strike
NumberBaseball_test.py
import unittest from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): def assertGuessResult(self, correct_answer, guess_answer, right_chk_result): game = Game(correct_answer) game_chk_result = game.guess_checker(guess_answer) self.assertEqual(right_chk_result, game_chk_result) def test_guess_correct_answer_then_3S(self): self.assertGuessResult("369", "369", "3S") self.assertGuessResult("106", "106", "3S") def test_guess_all_wrong_answer_then_OUT(self): self.assertGuessResult("369", "145", "OUT") self.assertGuessResult("106", "275", "OUT") def test_guess_just_one_right_number_right_location_then_1S(self): self.assertGuessResult("369", "382", "1S") self.assertGuessResult("106", "709", "1S") def test_guess_two_right_number_right_location_then_1S(self): self.assertGuessResult("369", "362", "2S") self.assertGuessResult("106", "706", "2S")
NumberBaseball.py
import random class Game: def __init__(self, correct_answer): if correct_answer: self.correct_anser = correct_answer else: self.correct_anser = random.sample(['0','1','2','3','4','5','6','7','8','9'],3) self.correct_anser = ''.join(self.correct_anser) def guess_checker(self, guess): # if guess == self.correct_anser: # return "3S" # out check out_checker = 0 for n in guess: if n not in self.correct_anser: out_checker += 1 if out_checker == 3: return "OUT" # strike check strike_checker = 0 for i in range(3): # 3자리 수 숫자 if guess[i] == self.correct_anser[i]: strike_checker += 1 return f"{strike_checker}S"
만들면서 생각난 것
- 생각해보니 정답과 일치해도 3S고, 세자리가 모두 일치해도 3S라서
if guess == self.correct_answer는 없어도 되겠다는 생각이 들어서 주석처리 했습니다.네번째 테스트 : 숫자만 일치하고 자리 불일치하는 경우 Ball (+리펙토링)
(제가 리펙토링 하기 이전 과정도 -구태여-보고 싶으시다면 더보기를 눌러주세요)
더보기NumberBaseball_test.py
import unittest from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): def assertGuessResult(self, correct_answer, guess_answer, right_chk_result): game = Game(correct_answer) game_chk_result = game.guess_checker(guess_answer) self.assertEqual(right_chk_result, game_chk_result) def test_guess_correct_answer_then_3S(self): self.assertGuessResult("369", "369", "3S") self.assertGuessResult("106", "106", "3S") def test_guess_all_wrong_answer_then_OUT(self): self.assertGuessResult("369", "145", "OUT") self.assertGuessResult("106", "275", "OUT") def test_guess_just_one_right_number_right_location_then_1S(self): self.assertGuessResult("369", "382", "1S 0B") self.assertGuessResult("106", "709", "1S 0B") def test_guess_two_right_number_right_location_then_1S(self): self.assertGuessResult("369", "362", "2S 0B") self.assertGuessResult("106", "706", "2S 0B") def test_guess_one_right_number_but_wrong_location_then_1B(self): self.assertGuessResult("369", "238", "0S 1B") self.assertGuessResult("106", "079", "0S 1B") def test_guess_two_right_number_but_wrong_location_then_2B(self): self.assertGuessResult("369", "938", "0S 2B") self.assertGuessResult("106", "069", "0S 2B")
NumberBaseball.py
import random class Game: def __init__(self, correct_answer): if correct_answer: self.correct_anser = correct_answer else: self.correct_anser = random.sample(['0','1','2','3','4','5','6','7','8','9'],3) self.correct_anser = ''.join(self.correct_anser) def guess_checker(self, guess): if guess == self.correct_anser: return "3S" # out check out_checker = 0 for n in guess: if n not in self.correct_anser: out_checker += 1 if out_checker == 3: return "OUT" # ball check ball_checker = 0 for n in guess: if n in self.correct_anser: ball_checker += 1 # strike check strike_checker = 0 for i in range(3): # 3자리 수 숫자 if guess[i] == self.correct_anser[i]: strike_checker += 1 # revise ball check ball_checker -= strike_checker return f"{strike_checker}S {ball_checker}B"
NumberBaseball_test.py
import unittest # from unittest.mock import Mock, MagicMock, call from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): def assertGuessResult(self, correct_answer, guess_answer, right_chk_result): game = Game(correct_answer) game_chk_result = game.guess_checker(guess_answer) self.assertEqual(right_chk_result, game_chk_result) # 앞선 테스트 생략 def test_guess_one_right_number_but_wrong_location_then_1B(self): self.assertGuessResult("369", "238", "0S 1B") self.assertGuessResult("106", "079", "0S 1B") def test_guess_two_right_number_but_wrong_location_then_2B(self): self.assertGuessResult("369", "938", "0S 2B") self.assertGuessResult("106", "069", "0S 2B") def test_guess_three_right_number_but_wrong_location_then_3B(self): self.assertGuessResult("369", "936", "0S 3B") self.assertGuessResult("106", "061", "0S 3B")
NumberBaseball.py
import random class Game: def __init__(self, correct_answer): if correct_answer: self.correct_anser = correct_answer else: self.correct_anser = random.sample(['0','1','2','3','4','5','6','7','8','9'],3) self.correct_anser = ''.join(self.correct_anser) def guess_checker(self, guess): if guess == self.correct_anser: return "3S" # out/ball check out_checker = 0 ball_checker = 0 for n in guess: if n in self.correct_anser: ball_checker += 1 else: out_checker += 1 if out_checker == 3: return "OUT" # strike check strike_checker = 0 for i in range(3): # 3자리 수 숫자 if guess[i] == self.correct_anser[i]: strike_checker += 1 # revise ball check ball_checker -= strike_checker return f"{strike_checker}S {ball_checker}B"
만들면서 생각난 것
- 다시 생각해보니 지금 당장은 필요 없지만, ball과 strike를 세는 방식을 조금 복잡하게 하는게 추후에 더 편하겠다는 생각이 들었습니다.
- 그래서 세번째 테스트에서 주석처리했던 코드를 다시 살렸습니다.
- 나중에 느낀 것이지만, 여기서 단순히 TDD를 진행하지 않고, 살짝 더 복잡하게 했던게 추후에 도움이 된 느낌입니다.
- TDD의 방식과 장점은 바로 앞의 문제를 해결하는 방식으로 쉽고 빠르게 목적 프로그램 코드와 테스트 코드까지 한꺼번에 작성할 수 있다는 것입니다.
- 단, TDD 방식으로만 진행할 경우(바로 앞의 문제만을 해결하는 방식으로 개발할 경우) 결과적으로는 더 어려워 질 수 있겠다는 생각이 들었습니다. (TDD는 절대 만능이 아니다)다섯번째 테스트 : 스트라이크와 볼이 혼재된 경우
NumberBaseball_test.py
import unittest from NumberBaseball import Game class NumberBaseballTest(unittest.TestCase): def assertGuessResult(self, correct_answer, guess_answer, right_chk_result): game = Game(correct_answer) game_chk_result = game.guess_checker(guess_answer) self.assertEqual(right_chk_result, game_chk_result) # 앞선 테스트 생략 def test_guess_mix_strike_and_ball(self): self.assertGuessResult("369", "639", "1S 2B") self.assertGuessResult("106", "016", "1S 2B") self.assertGuessResult("369", "962", "1S 1B") self.assertGuessResult("106", "716", "1S 1B")
NumberBaseball.py
# 네번째 테스트와 코드 동일 import random class Game: def __init__(self, correct_answer): if correct_answer: self.correct_anser = correct_answer else: self.correct_anser = random.sample(['0','1','2','3','4','5','6','7','8','9'],3) self.correct_anser = ''.join(self.correct_anser) def guess_checker(self, guess): if guess == self.correct_anser: return "3S" # out/ball check out_checker = 0 ball_checker = 0 for n in guess: if n in self.correct_anser: ball_checker += 1 else: out_checker += 1 if out_checker == 3: return "OUT" # strike check strike_checker = 0 for i in range(3): # 3자리 수 숫자 if guess[i] == self.correct_anser[i]: strike_checker += 1 # revise ball check ball_checker -= strike_checker return f"{strike_checker}S {ball_checker}B"
반응형'Programing > TDD' 카테고리의 다른 글
파이썬 TDD 예제: 숫자야구게임 만들기 #2 (feat. unittest) (0) 2023.01.28 파이썬 TDD 예제: chapter 03 유료 서비스 만료일 계산기 (feat. unittest) (1) 2022.12.06 파이썬 TDD 예제: chapter 02 암호 검사기 (feat. unittest) (0) 2022.11.27 파이썬으로 TDD 진행해보기(feat. unittest 예제) (0) 2022.11.24 TDD(Test Driven Development) 사내스터디를 시작하며 (0) 2022.11.23