Programing/TDD

파이썬 TDD 예제: chapter 02 암호 검사기 (feat. unittest)

뇌님 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) 매 단계에서 코드의 제시순서는 (테스트코드→기능코드) 순입니다. 이유는 다음과 같습니다.

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