ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬 TDD 예제: chapter 03 유료 서비스 만료일 계산기 (feat. unittest)
    Programing/TDD 2022. 12. 6. 00:08
    반응형

    이 글은 아래의 스터디 도서의 내용 중,

    책의 개발 순서를 따라가지 않고 자체적으로 실습한 TDD 과정입니다.

    따라서 개발 내용 및 테스트 구조가 불완전 할 수 있음을 먼저 안내 드립니다.

    (부족한 점이나 제가 생각해보지 못한 점에 대한 충고 주시면 정말 감사드리겠습니다.)

     

    테스트 주도 개발 시작하기 - YES24

    TDD(Test-Driven Development)는 테스트부터 시작한다. 구현을 먼저 하고 나중에 테스트하는 것이 아니라 먼저 테스트를 하고 그다음에 구현한다. 구현 코드가 없는데 어떻게 테스트할 수 있을까? 여기

    www.yes24.com

     

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

     

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

    스터디 도서의 내용을 실습하기 이전에, 파이썬으로 TDD를 진행할 수 있는지 자체를 먼저 확인해 봐야겠습니다. 엄밀히 따지면, 이 글의 제목은 잘못되었습니다. "TDD 진행해보기"가 아니라, "코드

     

     

    유료 서비스 만료일 계산기 전제조건 (규칙)

    • 서비스를 사용하려면 매달 1만원을 선불로 납부한다. 납부일 기준으로 한 달 뒤가 서비스 만료일이 된다.
    • 2개월 이상 요금을 납부할 수 있다.
    • 10만원을 납부하면 서비스를 1년 제공한다
    [(추가적으로 생각해본) 고려할 요건, 테스트]
    1) 한번에 납부하는 요금
    - 딱 1달 치(1만원)
    - 2~9개월 치(2~9만원)
    - 딱 1년 치(10만원)
    - 13~19개월치(11~19만원)
    - 그 이상
    
    2) 특수조건
    - 2월 29일 (2020년에 존재)
        - 2월 29일이 낀 경우
        - 2월 29일에 시작
        - 2월 29일에 끝남
    
    3) N번째 납부하는 요금

     

     

    첫번째 테스트 : 1만원 넣으면 한달 뒤 만료 (+ 리팩토링)

    ExpiryDateCalculator_test.py

    import unittest
    from datetime import datetime
    from ExpiryDateCalculator import Calculator
    
    
    class ExpiryDateCalculatorTest(unittest.TestCase):
        cal = Calculator()
    
        def test_pay_10000_then_1_month(self):
            pay_amount = 10000
    
            pay_date = datetime.strptime("221205", "%y%m%d")
            estimate_expiriy_date = self.cal.calculateExpiryDate(pay_date, pay_amount)
            real_expiry_date = datetime.strptime("230105", "%y%m%d")
            self.assertEqual(estimate_expiriy_date, real_expiry_date)
    
            pay_date = datetime.strptime("220131", "%y%m%d")
            estimate_expiriy_date = self.cal.calculateExpiryDate(pay_date, pay_amount)
            real_expiry_date = datetime.strptime("220228", "%y%m%d")
            self.assertEqual(estimate_expiriy_date, real_expiry_date)
    
            pay_date = datetime.strptime("230531", "%y%m%d")
            estimate_expiriy_date = self.cal.calculateExpiryDate(pay_date, pay_amount)
            real_expiry_date = datetime.strptime("230630", "%y%m%d")
            self.assertEqual(estimate_expiriy_date, real_expiry_date)

    ExpiryDateCalculator.py

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    
    class Calculator:
        def calculateExpiryDate(self, pay_date, pay_amount):
            return pay_date + relativedelta(months=1)

     

     

    두번째 테스트 : 2~9만원 내는 경우 + 2월 29일이 낀 경우

    ExpiryDateCalculator_test.py

    import unittest
    from datetime import datetime
    from ExpiryDateCalculator import Calculator
    
    
    class ExpiryDateCalculatorTest(unittest.TestCase):
        cal = Calculator()
    
        def assertExpiryDate(self, pay_date, pay_amount, real_expiry_date):
            pay_date = datetime.strptime(pay_date, "%y%m%d")
            real_expiry_date = datetime.strptime(real_expiry_date, "%y%m%d")
            estimate_expiry_date = self.cal.calculateExpiryDate(pay_date, pay_amount)
            self.assertEqual(estimate_expiry_date, real_expiry_date)
    
        def test_pay_10000_then_1_month(self):
            self.assertExpiryDate("221205", 10000, "230105")
            self.assertExpiryDate("220131", 10000, "220228")
            self.assertExpiryDate("230531", 10000, "230630")
    
        def test_pay_10000_then_1_month_but_0229_existed(self):
            self.assertExpiryDate("200131", 10000, "200229")
            self.assertExpiryDate("200130", 10000, "200229")
            self.assertExpiryDate("240130", 10000, "240229")
    
        def test_pay_20000to90000_then_2to9_month(self):
            self.assertExpiryDate("220131", 20000, "220331")
            self.assertExpiryDate("230531", 40000, "230930")
    
        def test_pay_20000to90000_then_2to9_month_but_2029_existed(self):
            self.assertExpiryDate("200112", 60000, "200712")
            self.assertExpiryDate("231031", 40000, "240229")
            self.assertExpiryDate("230530", 90000, "240229")

    ExpiryDateCalculator.py

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    
    class Calculator:
        def calculateExpiryDate(self, pay_date, pay_amount):
            month_amount = pay_amount/10000
            return pay_date + relativedelta(months=month_amount)

     

    [(추가적으로 생각해본) 고려할 요건, 테스트]
    1) 처음으로 납부하는 사람이 한번에 납부하는 경우
    - 딱 1달 치(1만원)  -> 해결
    - 2~9개월 치(2~9만원) -> 이때도 특수조건(2월29일)해보기, 이때 되면 다른때는 추가로 테스트 안해봐도 됨 -> 해결
    - 딱 1년 치(10만원)
    - 13~19개월치(11~19만원)
    - 그 이상
    
    2) 특수조건
    - 2월 29일 (2020년, 2024년에 존재)
        - 2월 29일이 낀 경우
        - 2월 29일에 시작(다시 생각해보니 테스트 할 필요 없음 -> 그래도 일단 한번 해보긴 함)
        - 2월 29일에 끝남
    
    3) N번째 납부하는 사람의 경우
    
    4) 예외처리
    - 납부금액이 1만원 단위가 아닌 경우 ex: 15000
    - 납부금액이 0원, 마이너스로 입력된 경우

     

     

    세번째 테스트 : 예외처리 - 0원이나 음수를 지불하려고 하는 경우 + 10000원 단위가 아닌 금액을 지불하려고 하는 경우

    ExpiryDateCalculator_test.py

    import unittest
    from datetime import datetime
    from ExpiryDateCalculator import Calculator
    
    
    class ExpiryDateCalculatorTest(unittest.TestCase):
        cal = Calculator()
    
        def assertExpiryDate(self, pay_date, pay_amount, real_expiry_date):
            pay_date = datetime.strptime(pay_date, "%y%m%d")
            if real_expiry_date != "INVALID":
                real_expiry_date = datetime.strptime(real_expiry_date, "%y%m%d")
            estimate_expiry_date = self.cal.calculateExpiryDate(pay_date, pay_amount)
            self.assertEqual(estimate_expiry_date, real_expiry_date)
    
        def test_pay_10000_then_1_month(self):
            self.assertExpiryDate("221205", 10000, "230105")
            self.assertExpiryDate("220131", 10000, "220228")
            self.assertExpiryDate("230531", 10000, "230630")
    
        def test_pay_10000_then_1_month_but_0229_existed(self):
            self.assertExpiryDate("200131", 10000, "200229")
            self.assertExpiryDate("200130", 10000, "200229")
            self.assertExpiryDate("240130", 10000, "240229")
    
        def test_pay_20000to90000_then_2to9_month(self):
            self.assertExpiryDate("220131", 20000, "220331")
            self.assertExpiryDate("230531", 40000, "230930")
    
        def test_pay_20000to90000_then_2to9_month_but_2029_existed(self):
            self.assertExpiryDate("200112", 60000, "200712")
            self.assertExpiryDate("231031", 40000, "240229")
            self.assertExpiryDate("230530", 90000, "240229")
    
        def test_pay_0_or_minus_then_Invalid(self):
            self.assertExpiryDate("200112", -10000, "INVALID")
            self.assertExpiryDate("231031", 0, "INVALID")
    
        def test_pay_that_not_divided_by_10000_then_INVALID(self):
            self.assertExpiryDate("231031", 15000, "INVALID")
            self.assertExpiryDate("221231", 27000, "INVALID")

    ExpiryDateCalculator.py

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    
    class Calculator:
        def calculateExpiryDate(self, pay_date, pay_amount):
            if pay_amount <= 0:
                return "INVALID"
    
            if (pay_amount % 10000) > 0:
                return "INVALID"
    
            month_amount = pay_amount/10000
            return pay_date + relativedelta(months=month_amount)

     

    [(추가적으로 생각해본) 고려할 요건, 테스트]
    1) 처음으로 납부하는 사람이 한번에 납부하는 경우
    - 딱 1달 치(1만원)  -> 완료
    - 2~9개월 치(2~9만원) -> 이때도 특수조건(2월29일)해보기, 이때 되면 다른때는 추가로 테스트 안해봐도 됨 -> 완료
    - 딱 1년 치(10만원)
    - 13~19개월치(11~19만원)
    - 그 이상
    
    2) 특수조건
    - 2월 29일 (2020년, 2024년에 존재)
        - 2월 29일이 낀 경우 -> 완료
        - 2월 29일에 끝남 -> 완료
    
    3) N번째 납부하는 사람의 경우
    
    4) 예외처리
    - 납부금액이 1만원 단위가 아닌 경우 ex: 15000 -> 완료
    - 납부금액이 0원, 마이너스로 입력된 경우 -> 완료

     

     

    네번째 테스트 : 10만원 납부하면 10개월이 아닌 1년 뒤 만료

    ExpiryDateCalculator_test.py

    import unittest
    from datetime import datetime
    from ExpiryDateCalculator import Calculator
    
    
    class ExpiryDateCalculatorTest(unittest.TestCase):
        cal = Calculator()
    
        def assertExpiryDate(self, pay_date, pay_amount, real_expiry_date):
            pay_date = datetime.strptime(pay_date, "%y%m%d")
            if real_expiry_date != "INVALID":
                real_expiry_date = datetime.strptime(real_expiry_date, "%y%m%d")
            estimate_expiry_date = self.cal.calculateExpiryDate(pay_date, pay_amount)
            self.assertEqual(estimate_expiry_date, real_expiry_date)
    
        # 앞선 다른 테스트 생략
    
        def test_pay_100000_then_1_year(self):
            self.assertExpiryDate("230101", 100000, "240101")
            self.assertExpiryDate("240215", 100000, "250215")

    ExpiryDateCalculator.py

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    
    class Calculator:
        def calculateExpiryDate(self, pay_date, pay_amount):
            if pay_amount <= 0:
                return "INVALID"
    
            if (pay_amount % 10000) > 0:
                return "INVALID"
    
            month_amount = pay_amount/10000
    
            if (month_amount / 10) >= 1:
                month_amount += 2
    
            return pay_date + relativedelta(months=month_amount)

     

     

    다섯번째 테스트 : 11~19만원, 20만원 이상 납부하는 경우

    ExpiryDateCalculator_test.py

    import unittest
    from datetime import datetime
    from ExpiryDateCalculator import Calculator
    
    
    class ExpiryDateCalculatorTest(unittest.TestCase):
        cal = Calculator()
    
        def assertExpiryDate(self, pay_date, pay_amount, real_expiry_date):
            pay_date = datetime.strptime(pay_date, "%y%m%d")
            if real_expiry_date != "INVALID":
                real_expiry_date = datetime.strptime(real_expiry_date, "%y%m%d")
            estimate_expiry_date = self.cal.calculateExpiryDate(pay_date, pay_amount)
            self.assertEqual(estimate_expiry_date, real_expiry_date)
    
        # 앞선 다른 테스트 생략
    
        def test_pay_110000to190000_then_1_year_and_2to9_month(self):
            self.assertExpiryDate("230101", 130000, "240401")
            self.assertExpiryDate("200331", 180000, "211130")
    
        def test_pay_more_than_200000(self):
            self.assertExpiryDate("220101", 200000, "240101")
            self.assertExpiryDate("220101", 250000, "240601")
            self.assertExpiryDate("140130", 610000, "200229")

    ExpiryDateCalculator.py

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    
    class Calculator:
        def calculateExpiryDate(self, pay_date, pay_amount):
            if pay_amount <= 0:
                return "INVALID"
    
            if (pay_amount % 10000) > 0:
                return "INVALID"
    
            month_amount = pay_amount/10000
    
            bonus_amount = int(month_amount / 10)*2
            month_amount += bonus_amount
    
            return pay_date + relativedelta(months=month_amount)

     

     

    여섯번째 테스트 : 서비스만료 이전에 추가로 납부하는 경우

    ExpiryDateCalculator_test.py

    import unittest
    from datetime import datetime
    from ExpiryDateCalculator import Calculator
    
    
    class ExpiryDateCalculatorTest(unittest.TestCase):
        cal = Calculator()
    
        def assertExpiryDate(self, pay_date, pay_amount, real_expiry_date, last_expiry_date=None):
            pay_date = datetime.strptime(pay_date, "%y%m%d")
            if real_expiry_date != "INVALID":
                real_expiry_date = datetime.strptime(real_expiry_date, "%y%m%d")
    
            estimate_expiry_date = self.cal.calculateExpiryDate(pay_date, pay_amount, last_expiry_date)
    
            self.assertEqual(estimate_expiry_date, real_expiry_date)
    
        # 앞선 테스트 생략
    
        def test_pay_before_expiry_date(self):
            self.assertExpiryDate("220115", 10000, "220217", "220117")  # 220117에 만료되는 사람이 220115에 1만원 납부
            self.assertExpiryDate("231007", 110000, "250101", "231201")  # 231201에 만료되는 사람이 231007에 11만원 납부

    ExpiryDateCalculator.py

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    
    class Calculator:
        def calculateExpiryDate(self, pay_date, pay_amount, last_expiry_date=None):
            if pay_amount <= 0:
                return "INVALID"
    
            if (pay_amount % 10000) > 0:
                return "INVALID"
    
            month_amount = pay_amount/10000
    
            bonus_amount = int(month_amount / 10)*2
            month_amount += bonus_amount
    
            if last_expiry_date != None:
                pay_date = datetime.strptime(last_expiry_date, "%y%m%d")
    
            return pay_date + relativedelta(months=month_amount)
    [(추가적으로 생각해본) 고려할 요건, 테스트]
    1) 처음으로 납부하는 사람이 한번에 납부하는 경우
    - 딱 1달 치(1만원)  -> 완료
    - 2~9개월 치(2~9만원) -> 이때도 특수조건(2월29일)해보기, 이때 되면 다른때는 추가로 테스트 안해봐도 됨 -> 완료
    - 딱 1년 치(10만원)  -> 완료
    - 13~19개월치(11~19만원)  -> 완료
    - 그 이상  -> 완료
    
    2) 특수조건
    - 2월 29일 (2020년, 2024년에 존재)
        - 2월 29일이 낀 경우 -> 완료
        - 2월 29일에 끝남 -> 완료
    
    3) N번째 납부하는 사람의 경우
    - 이전에 납부했던 금액에 대한 만료 당월에 추가로 납부하는 경우 -> 완료
    - 이전에 납부했던 금액에 대한 만료 당월이 아닌데 또 납부하는 경우 -> 완료
    - 근데 만료일(년월일 중 일)이 시작일자랑 다른 경우 (31일에 납부 시작했던 사람이 이번 납부 만료일은 30일)
    
    4) 예외처리
    - 납부금액이 1만원 단위가 아닌 경우 ex: 15000 -> 완료
    - 납부금액이 0원, 마이너스로 입력된 경우 -> 완료

     

     

    일곱번째 테스트 : N번째 납부하는 사람의 시작일자와 현재의 서비스 만료일이 다른 경우

    ExpiryDateCalculator_test.py

    import unittest
    from datetime import datetime
    from ExpiryDateCalculator import Calculator
    
    
    class ExpiryDateCalculatorTest(unittest.TestCase):
        cal = Calculator()
    
        def assertExpiryDate(self, pay_date, pay_amount, real_expiry_date, last_expiry_date=None, first_pay_date=None):
            pay_date = datetime.strptime(pay_date, "%y%m%d")
            if real_expiry_date != "INVALID":
                real_expiry_date = datetime.strptime(real_expiry_date, "%y%m%d")
    
            if last_expiry_date:
                last_expiry_date = datetime.strptime(last_expiry_date, "%y%m%d")
    
            if first_pay_date:
                first_pay_date = datetime.strptime(first_pay_date, "%y%m%d")
    
            estimate_expiry_date = self.cal.calculateExpiryDate(pay_date, pay_amount, last_expiry_date, first_pay_date)
    
            self.assertEqual(estimate_expiry_date, real_expiry_date)
    
        def test_pay_10000_then_1_month(self):
            self.assertExpiryDate("221205", 10000, "230105")
            self.assertExpiryDate("220131", 10000, "220228")
            self.assertExpiryDate("230531", 10000, "230630")
    
        def test_pay_10000_then_1_month_but_0229_existed(self):
            self.assertExpiryDate("200131", 10000, "200229")
            self.assertExpiryDate("200130", 10000, "200229")
            self.assertExpiryDate("240130", 10000, "240229")
    
        def test_pay_20000to90000_then_2to9_month(self):
            self.assertExpiryDate("220131", 20000, "220331")
            self.assertExpiryDate("230531", 40000, "230930")
    
        def test_pay_20000to90000_then_2to9_month_but_2029_existed(self):
            self.assertExpiryDate("200112", 60000, "200712")
            self.assertExpiryDate("231031", 40000, "240229")
            self.assertExpiryDate("230530", 90000, "240229")
    
        def test_pay_0_or_minus_then_Invalid(self):
            self.assertExpiryDate("200112", -10000, "INVALID")
            self.assertExpiryDate("231031", 0, "INVALID")
    
        def test_pay_that_not_divided_by_10000_then_INVALID(self):
            self.assertExpiryDate("231031", 15000, "INVALID")
            self.assertExpiryDate("221231", 27000, "INVALID")
    
        def test_pay_100000_then_1_year(self):
            self.assertExpiryDate("230101", 100000, "240101")
            self.assertExpiryDate("240215", 100000, "250215")
    
        def test_pay_110000to190000_then_1_year_and_2to9_month(self):
            self.assertExpiryDate("230101", 130000, "240401")
            self.assertExpiryDate("200331", 180000, "211130")
    
        def test_pay_more_than_200000(self):
            self.assertExpiryDate("220101", 200000, "240101")
            self.assertExpiryDate("220101", 250000, "240601")
            self.assertExpiryDate("140130", 610000, "200229")
    
        def test_pay_before_expiry_date(self):
            self.assertExpiryDate("220115", 10000, "220217", last_expiry_date="220117", first_pay_date="210617")
            self.assertExpiryDate("231007", 110000, "250101", last_expiry_date="231201", first_pay_date="200401")
    
        def test_pay_before_expiry_date_but_first_pay_date_is_different(self):
            self.assertExpiryDate("220115", 10000, "220331", last_expiry_date="220228", first_pay_date="190331")
            self.assertExpiryDate("190615", 100000, "210228", last_expiry_date="200229", first_pay_date="190430")

    ExpiryDateCalculator.py

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    
    class Calculator:
        def calculateExpiryDate(self, pay_date, pay_amount, last_expiry_date=None, first_pay_date=None):
            if pay_amount <= 0:
                return "INVALID"
    
            if (pay_amount % 10000) > 0:
                return "INVALID"
    
            month_amount = pay_amount/10000
    
            bonus_amount = int(month_amount / 10)*2
            month_amount += bonus_amount
    
            if (last_expiry_date != None) & (first_pay_date != None):
                relative_years = relativedelta(last_expiry_date, first_pay_date).years
                relative_months = relativedelta(last_expiry_date, first_pay_date).months
                return first_pay_date + relativedelta(years=relative_years) + relativedelta(months=month_amount+relative_months)
    
            return pay_date + relativedelta(months=month_amount)

     


     

    이 글에서는 코드만을 다룹니다

    1) 상세한 내용을 다루지 않고 코드만 올리는 이유는 다음과 같습니다.

    • 왜 다음과 같은 과정으로 코드를 개발하는지 직접 책을 읽고 저자의 생각에 공감하고 비평하는 것이 실력향상에 도움이 되기 때문에 세부 내용은 작성하지 않습니다.
    • 지적재산권을 침해하지 않기 위해 세부 내용은 작성하지 않습니다.

     

    2) 그럼 굳이 책만 읽어도 해결 되는데 이 글이 무슨 소용이라고 글을 올리는가?에 대한 이유는 다음과 같습니다.

    • 이 책은 자바 기반의 책이기 때문에, 파이썬 언어만을 다뤄본 분들에겐 약간의 어려움이 느껴질 수 있습니다.
      (그래도 한번 펼쳐보는걸 추천드려요! 자바에 친숙하지 않더라도 금방 파악하실 수 있게 쉽게 쓰인 코드라고 생각합니다.)
      조금이나마 도움이 될까 싶어 파이썬으로 동일한 과정을 진행하는 코드를 공유하고자 합니다.

     

    3) 매 단계에서 코드의 제시순서는 (테스트코드→기능코드) 순입니다. 이유는 다음과 같습니다.

    • 스터디 도서에서는 테스트코드를 먼저 작성하고, 이후 테스트를 통과할 수 있는 기능을 구현하라고 말합니다.
    • 그 취지에 맞게, 테스트 코드를 상위에 두고, 기능코드를 후순을 둡니다.
    반응형

    댓글

Designed by Tistory.