ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬 TDD 예제: chapter 02 암호 검사기 (feat. unittest)
    Programing/TDD 2022. 11. 27. 19:41
    반응형

    이 글은 아래의 스터디 도서의 내용 중, 제가 자바 코드를 파이썬 코드로 변환하고,

    코드만 올린 글입니다.

     

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

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

    www.yes24.com

     

     

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

     

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

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

    brain-nim.tistory.com

     

     

    암호 검사기 전제조건 (규칙)

    • 검사할 규칙은 아래의 3가지
      • 길이가 8글자 이상
      • 0~9 사이의 숫자를 포함
      • 대문자 포함
    • 세 규칙을 모두 충족 = 강함
    • 2개의 규칙을 충족 = 보통
    • 1개 이하의 규칙을 충족 = 약함

     

     

    첫 번째 테스트: 모든 규칙을 충족하는 경우

    PasswordStrengthMeter_test.py

    import unittest
    from PasswordStrengthMeter import Meter
    
    
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        def test_meetsAllCriteria_Then_strong(self):
            meter = Meter()
            result = meter.meter("ab12!@AB")
            self.assertEqual("STRONG", result)
            result2 = meter.meter("abc1!Add")
            self.assertEqual("STRONG", result2)
    
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            return "STRONG"

     

     

    두 번째 테스트: 길이만 8글자 미만이고 나머지 조건은 충족하는 경우

    PasswordStrengthMeter_test.py

    import unittest
    from PasswordStrengthMeter import Meter
    
    
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        def test_meetsAllCriteria_Then_strong(self):
            meter = Meter()
            result = meter.meter("ab12!@AB")
            self.assertEqual("STRONG", result)
            result2 = meter.meter("abc1!Add")
            self.assertEqual("STRONG", result2)
    
        def test_meetsOtherCriteria_except_for_Length_Then_Normal(self):
            meter = Meter()
            result = meter.meter("ab12!@A")
            self.assertEqual("NORMAL", result)
            result2 = meter.meter("Ab12!c")
            self.assertEqual("NORMAL", result2)
    
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if len(password) < 8:
                return "NORMAL"
            return "STRONG"

     

     

    세 번째 테스트: 숫자를 포함하지 않고 모든 조건은 충족하는 경우

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        def test_meetsAllCriteria_Then_strong(self):
            meter = Meter()
            result = meter.meter("ab12!@AB")
            self.assertEqual("STRONG", result)
            result2 = meter.meter("abc1!Add")
            self.assertEqual("STRONG", result2)
    
        def test_meetsOtherCriteria_except_for_Length_Then_Normal(self):
            meter = Meter()
            result = meter.meter("ab12!@A")
            self.assertEqual("NORMAL", result)
            result2 = meter.meter("Ab12!c")
            self.assertEqual("NORMAL", result2)
    
        def test_meetsOtherCriteria_except_for_Number_Then_Normal(self):
            meter = Meter()
            result = meter.meter("ab!@ABqwer")
            self.assertEqual("NORMAL", result)
    
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if len(password) < 8:
                return "NORMAL"
    
            containsNum = self.meetsContainingNumberCrieteria(password)
    
            if not containsNum:
                return "NORMAL"
    
            return "STRONG"
    
        def meetsContainingNumberCrieteria(self, password):
            for ch in password:
                if (ch >= '0') & (ch <= '9'):
                    return True
            return False

     

     

    코드 정리: 테스트 코드 정리

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        meter = Meter()
    
        def assertStrength(self, password, strength):
            result = self.meter.meter(password)
            self.assertEqual(strength, result)
    
        def test_meetsAllCriteria_Then_strong(self):
            self.assertStrength("ab12!@AB", "STRONG")
            self.assertStrength("abc1!Add", "STRONG")
    
        def test_meetsOtherCriteria_except_for_Length_Then_Normal(self):
            self.assertStrength("ab12!@A", "NORMAL")
            self.assertStrength("Ab12!c", "NORMAL")
    
        def test_meetsOtherCriteria_except_for_Number_Then_Normal(self):
            self.assertStrength("ab!@ABqwer", "NORMAL")
    
    
    if __name__ == '__main__':
        unittest.main()

     

     

    네 번째 테스트: 값이 없는 경우

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        meter = Meter()
    
        def assertStrength(self, password, strength):
            result = self.meter.meter(password)
            self.assertEqual(strength, result)
    
        def test_meetsAllCriteria_Then_strong(self):
            self.assertStrength("ab12!@AB", "STRONG")
            self.assertStrength("abc1!Add", "STRONG")
    
        def test_meetsOtherCriteria_except_for_Length_Then_Normal(self):
            self.assertStrength("ab12!@A", "NORMAL")
            self.assertStrength("Ab12!c", "NORMAL")
    
        def test_meetsOtherCriteria_except_for_Number_Then_Normal(self):
            self.assertStrength("ab!@ABqwer", "NORMAL")
    
        def test_nullInput_tent_Invalid(self):
            self.assertStrength(None, "INVALID")
    
        def test_emptyInput_Then_Invalid(self):
            self.assertStrength("", "INVALID")
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            if len(password) < 8:
                return "NORMAL"
    
            containsNum = self.meetsContainingNumberCrieteria(password)
    
            if not containsNum:
                return "NORMAL"
    
            return "STRONG"
    
        def meetsContainingNumberCrieteria(self, password):
            for ch in password:
                if (ch >= '0') & (ch <= '9'):
                    return True
            return False

     

     

    다섯 번째 테스트: 대문자를 포함하지 않고 나머지 조건을 충족하는 경우

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        meter = Meter()
    
        def assertStrength(self, password, strength):
            result = self.meter.meter(password)
            self.assertEqual(strength, result)
    
        def test_meetsAllCriteria_Then_strong(self):
            self.assertStrength("ab12!@AB", "STRONG")
            self.assertStrength("abc1!Add", "STRONG")
    
        def test_meetsOtherCriteria_except_for_Length_Then_Normal(self):
            self.assertStrength("ab12!@A", "NORMAL")
            self.assertStrength("Ab12!c", "NORMAL")
    
        def test_meetsOtherCriteria_except_for_Number_Then_Normal(self):
            self.assertStrength("ab!@ABqwer", "NORMAL")
    
        def test_nullInput_tent_Invalid(self):
            self.assertStrength(None, "INVALID")
    
        def test_emptyInput_Then_Invalid(self):
            self.assertStrength("", "INVALID")
    
        def test_meetsOtherCriteria_excpet_for_Uppercase_Then_Normal(self):
            self.assertStrength("ab12!@df", "NORMAL")
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            if len(password) < 8:
                return "NORMAL"
    
            containsNum = self.meetsContainingNumberCriteria(password)
            containsUpp = self.meetsContainingUppercaseCriteria(password)
    
            if not containsNum:
                return "NORMAL"
    
            if not containsUpp:
                return "NORMAL"
    
            return "STRONG"
    
        def meetsContainingNumberCriteria(self, password):
            for ch in password:
                if (ch >= '0') & (ch <= '9'):
                    return True
            return False
    
        def meetsContainingUppercaseCriteria(self, password):
            for ch in password:
                if ch.isupper():
                    return True
            return False
    

     

     

    여섯 번째 테스트: 길이가 8글자 이상인 조건만 충족하는 경우

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        meter = Meter()
    
        # 기타 테스트 생략
        
        def test_meetsOnlyLengthCriteria_Then_Weak(self):
            self.assertStrength("abdefghi", "WEAK")
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            lengthEnough = (len(password) >= 8)
            containsNum = self.meetsContainingNumberCriteria(password)
            containsUpp = self.meetsContainingUppercaseCriteria(password)
    
            if (lengthEnough) & (not containsNum) & (not containsUpp):
                return "WEAK"
    
            if not lengthEnough: return "NORMAL"
            if not containsNum: return "NORMAL"
            if not containsUpp: return "NORMAL"
    
            return "STRONG"
    
        def meetsContainingNumberCriteria(self, password):
            for ch in password:
                if (ch >= '0') & (ch <= '9'):
                    return True
            return False
    
        def meetsContainingUppercaseCriteria(self, password):
            for ch in password:
                if ch.isupper():
                    return True
            return False

     

     

    일곱 번째 테스트: 숫자 포함 조건만 충족하는 경우

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        meter = Meter()
    
        # 기타 테스트 생략
    
        def test_meetsOnlyNumCriteria_Then_Weak(self):
            self.assertStrength("12345", "WEAK")
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            lengthEnough = (len(password) >= 8)
            containsNum = self.meetsContainingNumberCriteria(password)
            containsUpp = self.meetsContainingUppercaseCriteria(password)
    
            if (lengthEnough) & (not containsNum) & (not containsUpp):
                return "WEAK"
            if (not lengthEnough) & (containsNum) & (not containsUpp):
                return "WEAK"
    
            if not lengthEnough: return "NORMAL"
            if not containsNum: return "NORMAL"
            if not containsUpp: return "NORMAL"
    
            return "STRONG"
    
        # meetsContainingNumberCriteria 생략
        # meetsContainingUppercaseCriteria 생략

     

     

    여덟 번째 테스트: 대문자 포함 조건만 충족하는 경우

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        meter = Meter()
    
        # 기타 테스트 생략
    
        def test_meetsOnlyUpperCriteria_Then_Weak(self):
            self.assertStrength("ABZEF", "WEAK")
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            lengthEnough = (len(password) >= 8)
            containsNum = self.meetsContainingNumberCriteria(password)
            containsUpp = self.meetsContainingUppercaseCriteria(password)
    
            if (lengthEnough) & (not containsNum) & (not containsUpp):
                return "WEAK"
            if (not lengthEnough) & (containsNum) & (not containsUpp):
                return "WEAK"
            if (not lengthEnough) & (not containsNum) & (containsUpp):
                return "WEAK"
    
            if not lengthEnough: return "NORMAL"
            if not containsNum: return "NORMAL"
            if not containsUpp: return "NORMAL"
    
            return "STRONG"
    
        # meetsContainingNumberCriteria 생략
        # meetsContainingUppercaseCriteria 생략

     

     

    코드 정리: meter() 메서드 리팩토링

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            metCounts = 0
    
            if (len(password) >= 8): metCounts += 1
            if self.meetsContainingNumberCriteria(password): metCounts += 1
            if self.meetsContainingUppercaseCriteria(password): metCounts += 1
    
            if metCounts == 1: return "WEAK"
            elif metCounts == 2: return "NORMAL"
    
            return "STRONG"
    
        def meetsContainingNumberCriteria(self, password):
            for ch in password:
                if (ch >= '0') & (ch <= '9'):
                    return True
            return False
    
        def meetsContainingUppercaseCriteria(self, password):
            for ch in password:
                if ch.isupper():
                    return True
            return False

     

     

    아홉 번째 테스트: 아무 조건도 충족하지 않는 경우

    PasswordStrengthMeter_test.py

    # import 생략
    class PasswordStrengthMeterTest(unittest.TestCase):
    
        meter = Meter()
    
        # 기타 테스트 생략
    
        def test_meetsNoCriteria_Then_Weak(self):
            self.assertStrength("abc", "WEAK")
    
    if __name__ == '__main__':
        unittest.main()

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            metCounts = 0
    
            if (len(password) >= 8): metCounts += 1
            if self.meetsContainingNumberCriteria(password): metCounts += 1
            if self.meetsContainingUppercaseCriteria(password): metCounts += 1
    
            if metCounts <= 1: return "WEAK"
            elif metCounts == 2: return "NORMAL"
    
            return "STRONG"
    
        # meetsContainingNumberCriteria 생략
        # meetsContainingUppercaseCriteria 생략

     

     

    코드 정리: 코드 가독성 개선

    PasswordStrengthMeter.py

    class Meter:
        def meter(self, password):
            if (password == None) | (password == ""):
                return "INVALID"
    
            metCounts = self.getMetCriteriaCounts(password)
    
            if metCounts <= 1: return "WEAK"
            elif metCounts == 2: return "NORMAL"
    
            return "STRONG"
    
        def getMetCriteriaCounts(self, password):
            metCounts = 0
            if (len(password) >= 8): metCounts += 1
            if self.meetsContainingNumberCriteria(password): metCounts += 1
            if self.meetsContainingUppercaseCriteria(password): metCounts += 1
            return metCounts
    
        def meetsContainingNumberCriteria(self, password):
            for ch in password:
                if (ch >= '0') & (ch <= '9'):
                    return True
            return False
    
        def meetsContainingUppercaseCriteria(self, password):
            for ch in password:
                if ch.isupper():
                    return True
            return False

     


     

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

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

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

     

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

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

     

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

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

    댓글

Designed by Tistory.