파이썬 TDD 예제: chapter 02 암호 검사기 (feat. unittest)
이 글은 아래의 스터디 도서의 내용 중, 제가 자바 코드를 파이썬 코드로 변환하고,
코드만 올린 글입니다.
테스트 주도 개발 시작하기 - 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) 매 단계에서 코드의 제시순서는 (테스트코드→기능코드) 순입니다. 이유는 다음과 같습니다.
- 스터디 도서에서는 테스트코드를 먼저 작성하고, 이후 테스트를 통과할 수 있는 기능을 구현하라고 말합니다.
- 그 취지에 맞게, 테스트 코드를 상위에 두고, 기능코드를 후순을 둡니다.