Python自定义密码验证:重构与单元测试实战
本文详解Python中自定义密码验证功能的单元测试与重构过程。通过编写针对contains_character、is_valid_size及is_valid_password的测试用例,发现并修复了未检查有效特殊字符的逻辑缺陷。最终实现符合长度、大小写、数字及特殊字符要求的健壮密码验证逻辑。
本系列文章共分为三个部分。
第一部分介绍密码验证功能。
第二部分对密码验证函数进行重构。
第三部分对密码验证功能进行单元测试。
这是Python系列中自定义密码验证的第三部分,也是最后一部分。我们将重点讨论如何对密码验证功能进行单元测试。
下面是重构后的代码:
from string import (
ascii_lowercase, ascii_uppercase,
digits, punctuation, whitespace)
def contains_character(password: str = "", sack: str = "") -> bool:
has_char = False
for char in password:
if char in sack:
has_char = True
break
return has_char
def is_valid_size(password: str = "") -> bool:
MIN_SIZE = 6
MAX_SIZE = 20
password_size = len(password)
return MIN_SIZE <= password_size <= MAX_SIZE
def get_invalid_chars():
valid_chars = {'-', '_', '.', '!', '@', '#', '$', '^', '&', '(', ')'}
invalid_chars = set(punctuation + whitespace) - valid_chars
return "".join(invalid_chars)
def is_valid_password(password: str = "") -> bool:
try:
if not password:
return False
new_password = password.strip()
if not is_valid_size(new_password):
return False
invalid_chars = get_invalid_chars()
if contains_character(new_password, invalid_chars):
return False
if not contains_character(new_password, digits):
return False
if not contains_character(new_password, ascii_lowercase):
return False
if not contains_character(new_password, ascii_uppercase):
return False
return True
except:
return False我们的目标是为上述代码片段编写单元测试。通过这种方式,我们可以捕捉隐藏的错误,并在修复代码以通过测试的过程中持续进行重构。
在运行测试之前
在开始之前,有几点需要注意:
- 这将是一个单元测试。
- 我们将利用Python内置的unittest模块。
- 我们将分别测试contains_character、is_valid_size和is_valid_password函数。
- 测试文件命名为test.py,而上述代码片段可能位于app.py中(文件名由你决定)。
- 我们将参考以下测试用例。
测试contains_character
contains_character返回布尔值,即True或False。因此,我们可以使用assertTrue和assertFalse方法。
我们将测试以下情况:
- 当未传递密码和sack(无参数)时。
- 当字符"i"在字符串"python"中时。
- 当字符串"py"在字符串"python"中时。
- 当字符串"python"在字符串"python"中时。
某些情况,例如传递int作为password或list作为sack,我们不会在此测试。(你应该为此编写测试)。
TestContainsCharacter类
import unittest
from app import contains_character
class TestContainsCharacter(unittest.TestCase):
def test_empty_password_or_and_empty_sack(self):
self.assertFalse(contains_character())
def test_char_i_in_str_python(self):
self.assertFalse(contains_character("i", "python"))
def test_str_py_in_str_python(self):
self.assertTrue(contains_character("py", "python"))
def test_str_python_in_str_python(self):
self.assertTrue(contains_character("python", "python"))
if __name__ == "__main__":
unittest.main()我们可以按Ctrl+F5运行此脚本(test.py)而无需调试。我们可以通过以下方式运行脚本:python3 test.py 或 python3 -m unittest test.py。所有这些测试都应该通过。
测试is_valid_size
is_valid_size返回布尔值,即True或False。因此,我们可以使用assertTrue和assertFalse方法。
我们将测试以下情况:
- 对于空密码或未传递参数时。
- 四个字符的密码。
- 六个字符的密码。
- 十六个字符的密码。
- 二十个字符的密码。
- 21个字符的密码。
TestIsValidSize类
import unittest
from app import is_valid_size
class TestIsValidSize(unittest.TestCase):
def test_empty_password(self):
self.assertFalse(is_valid_size(""))
def test_4_char_password(self):
self.assertFalse(is_valid_size("pass"))
def test_6_char_password(self):
self.assertTrue(is_valid_size("passwd"))
def test_16_char_password(self):
self.assertTrue(is_valid_size("ThisIs1Password!"))
def test_20_char_password(self):
self.assertTrue(is_valid_size("ThisIs1Password!+20"))
def test_21_char_password(self):
self.assertFalse(is_valid_size("ThisIs1Password!+20&"))
if __name__ == "__main__":
unittest.main()所有这些测试都应该通过。
测试is_valid_password
is_valid_password返回布尔值,即True或False。因此,我们可以使用assertTrue和assertFalse方法。
我们将测试以下情况:
1. 对于空密码。
2. 三个字符的密码。
3. 十个字符的密码。
4. 二十个字符的密码。
5. 对于包含无效特殊字符(如分号)的密码。
6. 对于没有数字的密码。
7. 对于没有小写字母的密码。
8. 对于没有大写字母的密码。
9. 对于没有有效特殊字符的密码。
10. 对于有效的密码。
- 密码长度在[6-20]以内。
- 至少包含一个小写和大写字符。
- 至少包含一个数字。
- 不包含无效的特殊字符。
TestIsValidPassword类
class TestIsValidPassword(unittest.TestCase):
def test_empty_password(self):
self.assertFalse(is_valid_password())
def test_password_of_size_three(self):
self.assertFalse(is_valid_password("pas"))
def test_password_of_size_ten(self):
self.assertFalse(is_valid_password("Password12"))
self.assertTrue(is_valid_password("Password1_"))
def test_password_of_size_twenty(self):
self.assertFalse(is_valid_password("Password12Password_$"))
def test_password_with_invalid_special_character_semicolon(self):
self.assertFalse(is_valid_password("Password1_;"))
self.assertFalse(is_valid_password("Password1;"))
def test_password_with_no_digit(self):
self.assertFalse(is_valid_password("Password_"))
def test_password_with_no_lowercase(self):
self.assertFalse(is_valid_password("PASSWORD1_"))
def test_password_with_no_uppercase(self):
self.assertFalse(is_valid_password("password1_"))
def test_password_without_valid_special_character(self):
self.assertFalse(is_valid_password("Password1"))
def test_valid_password(self):
self.assertTrue(is_valid_password("Password1_"))
self.assertTrue(is_valid_password("PassWord34$"))
if __name__ == "__main__":
unittest.main()并非所有测试都通过了。这些测试用例本应失败——我们期望它们不会通过。因此,当我们期望False时却得到了True。这表明代码中存在缺陷或错误。
以下测试未通过:
- test_password_of_size_ten : self.assertFalse(is_valid_password("Password12")) 应该是False,因为即使长度有效,它也没有特殊字符。
- test_password_without_valid_special_character : self.assertFalse(is_valid_password("Password1")) 应该是False,因为没有有效的特殊字符。
is_valid_password函数未检查是否存在有效的特殊字符。它仅检查无效字符,但不检查有效字符。这是由有缺陷的假设造成的,即只要密码不包含无效字符,它就包含有效字符(包括有效的特殊字符)。
重构is_valid_password
既然我们已经指出了bug,我们应该做出更改并重新运行测试。
需要进行的更改:
在get_invalid_chars中,我们定义了有效特殊字符集合valid_chars。让我们使其对所有函数全局可见(例如,将其从get_invalid_chars函数中提取并放在文件顶部)。为了确保其他地方没有损坏,运行测试(我们预计有两种情况会失败)。请注意,即使我们移动valid_chars,get_invalid_chars也应能正常工作。
valid_chars是一个集合,它可以用作get_invalid_chars中的一组。contains_character需要string类型的sack作为参数。我们必须将valid_chars解析为string。让我们在下面创建一个函数get_valid_chars,返回string版本的valid_chars。
def get_valid_chars():
return "".join(valid_chars)
进行测试。
让我们检查is_valid_password中的有效字符,通过在return True语句中的try块内添加检查。
if not contains_character(new_password, get_valid_chars()):
return False
进行测试。现在,所有测试都通过了。万岁!!:clap::clap::clap:
这更多的是重新排列代码,使is_valid_password在另一种环境中自然运行良好。我们将按以下顺序重新排列is_valid_password的测试:size、lower case、upper case、digit、invalid special character 和 valid special character。
结论
is_valid_password在app.py中类似于以下代码片段:
from string import (ascii_lowercase, ascii_uppercase, digits, punctuation,
whitespace)
valid_chars = {'-', '_', '.', '!', '@', '#', '$', '^', '&', '(', ')'}
def contains_character(password: str = "", sack: str = "") -> bool:
has_char = False
for char in password:
if char in sack:
has_char = True
break
return has_char
def is_valid_size(password: str = "") -> bool:
MIN_SIZE = 6
MAX_SIZE = 20
password_size = len(password)
return MIN_SIZE <= password_size <= MAX_SIZE
def get_invalid_chars():
invalid_chars = set(punctuation + whitespace) - valid_chars
return "".join(invalid_chars)
def get_valid_chars():
return "".join(valid_chars)
def is_valid_password(password: str = "") -> bool:
try:
if not password:
return False
new_password = password.strip()
if not is_valid_size(new_password):
return False
if not contains_character(new_password, ascii_lowercase):
return False
if not contains_character(new_password, ascii_uppercase):
return False
if not contains_character(new_password, digits):
return False
if contains_character(new_password, get_invalid_chars()):
return False
if not contains_character(new_password, get_valid_chars()):
return False
return True
except:
return False单元测试在test.py中类似于以下代码片段:
import unittest
from app import (contains_character, is_valid_size, is_valid_password)
class TestContainsCharacter(unittest.TestCase):
def test_empty_password_or_and_empty_sack(self):
self.assertFalse(contains_character())
def test_char_i_in_str_python(self):
self.assertFalse(contains_character("i", "python"))
def test_str_py_in_str_python(self):
self.assertTrue(contains_character("py", "python"))
def test_str_python_in_str_python(self):
self.assertTrue(contains_character("python", "python"))
class TestIsValidSize(unittest.TestCase):
def test_empty_password(self):
self.assertFalse(is_valid_size(""))
def test_4_char_password(self):
self.assertFalse(is_valid_size("pass"))
def test_6_char_password(self):
self.assertTrue(is_valid_size("passwd"))
def test_16_char_password(self):
self.assertTrue(is_valid_size("ThisIs1Password!"))
def test_20_char_password(self):
self.assertTrue(is_valid_size("ThisIs1Password!/+20"))
def test_21_char_password(self):
self.assertFalse(is_valid_size("ThisIs1Password!/+20&"))
class TestIsValidPassword(unittest.TestCase):
def test_empty_password(self):
self.assertFalse(is_valid_password())
def test_password_of_size_three(self):
self.assertFalse(is_valid_password("pas"))
def test_password_of_size_ten(self):
self.assertFalse(is_valid_password("Password12"))
self.assertTrue(is_valid_password("Password1_"))
def test_password_of_size_twenty(self):
self.assertTrue(is_valid_password("Password12Password_$"))
def test_password_with_invalid_special_character_semicolon(self):
self.assertFalse(is_valid_password("Password1_;"))
self.assertFalse(is_valid_password("Password1;"))
def test_password_with_no_digit(self):
self.assertFalse(is_valid_password("Password_"))
def test_password_with_no_lowercase(self):
self.assertFalse(is_valid_password("PASSWORD1_"))
def test_password_with_no_uppercase(self):
self.assertFalse(is_valid_password("password1_"))
def test_password_without_valid_special_character(self):
self.assertFalse(is_valid_password("Password1"))
def test_valid_password(self):
self.assertTrue(is_valid_password("Password1_"))
self.assertTrue(is_valid_password("PassWord34$"))
if __name__ == "__main__":
unittest.main()以上就是详解Python中的自定义密码验证的详细内容,更多关于Python密码验证的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













