ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬 TDD 예제: 숫자야구게임 만들기 #1 (feat. unittest)
    Programing/TDD 2023. 1. 28. 02:03
    반응형

    TDD 연습으로 숫자야구게임 만들기를 많이 하길래,

    저희 사내스터디에서도 책갈이를 한 이후에 숫자야구게임을 각자 만들어보기로 했습니다.

     

    만들면서 느꼈던 점, 서로 피드백을 하면서 추가로 느낀 점들이 있었는데,

    이 내용들은 각각 중간중간에 서술하도록 하겠습니다.

     

    파이썬으로 TDD를 진행하는 기본적인 방식에 대해서는 아래의 글을 참조해 주세요

     

    파이썬으로 TDD 진행해보기(feat. unittest 예제)

    스터디 도서의 내용을 실습하기 이전에, 파이썬으로 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"

     

    반응형

    댓글

Designed by Tistory.