谈一次单元测试驱动代码重构
? ? ? ? 目前團隊并沒有QA崗,而且在很長一段時間內,可能也不會設立QA崗,所以我們需要RD保證代碼的質量。而鑒于人類天生的“惰性”,很多時候質量完全依賴于作者的能力以及職業素質。于是我在團隊內推動單元測試,并要求提升測試覆蓋率。雖然單元測試不能“根治”bug,但是它可以驅使代碼結構簡潔可測,為提升測試代碼覆蓋率奠定基礎,從而可以有效降低bug率。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 以下我將以工作中一個實際例子講解如何將一個不可測代碼變成更加合理且可測代碼。
class CheckLinkRequest:def execute(self):try:db = Db()app_links = db.query(AppLinks).filter(AppLinks.valid == True).all()LOG_DEBUG('app links data is {0}'.format(app_links))data_list = []if app_links:for _ in app_links:LOG_DEBUG('app links data is {0}'.format(_))user = db.query(AccountUser.email).filter(AccountUser.valid == True, AccountUser.id == _.user_id).all()LOG_DEBUG('user email is {0}'.format(user))data_list.append({"source": simplejson.dumps({'url': _.app_link, 'id': _.id, 'email': user[0][0]})})LOG_DEBUG('data list is {0}'.format(data_list))except Exception as e:LOG_ERROR('app link error {0}'.format(e))return JsonFuncResponser({'data': data_list})else:return JsonFuncResponser({'data': data_list})
? ? ? ? 這段代碼大致意思是:
- 從AppLinks表中檢索出所有有效數據(第5行)
- 遍歷1中結果,查詢每個信息對應的email(第11,12行)
- 將1中渠道的link信息和2中渠道的email信息組裝成一條記錄(第14,15行)
? ? ? ? 這段代碼有好幾個問題:
- 如果異常發生在第7行之前,執行到第19行時由于data_list未聲明而被使用,將拋出異常
- 兩處查詢數據庫可能產生的異常很不方便測試
- 第8行判斷沒有必要,而且造成一層嵌套。如果返回的數組,則可以進入異常處理;如果返回空數組,第21行也能正確處理。
- 第15行想當然的認為user是個二維數組,從而導致拋出異常
? ? ? ? 我們開始著手對這段代碼進行改造。
? ? ? ? 依據“職責單一原則”,execute方法包含了太多功能,我們需要將其進行拆解重組:
class CheckLinkRequest:def __init__(self):self._db = Nonedef _init_db(self):if not self._db:self._db = Db()def _get_all_valid_applinks(self):self._init_db()return self._db.query_list(AppLinks.app_link, AppLinks.user_id, AppLinks.id).filter(AppLinks.valid == True).all()def _get_email_by_user_id(self, user_id):self._init_db()user = self._db.query_list(AccountUser.email).filter(AccountUser.valid == True, AccountUser.id == user_id).first()return user.emaildef _email_empty(self, user_id):LOG_WARNING("need to set email for user{0}".format(user_id))def _execute_with_exception(self):data_list = []app_links = self._get_all_valid_applinks()LOG_DEBUG('app links data is {0}'.format(app_links))for _ in app_links:LOG_DEBUG('app links data is {0}'.format(_))email = self._get_email_by_user_id(_.user_id)if not email:self._email_empty(_.user_id)LOG_DEBUG('user email is {0}'.format(email))data_list.append({"source": simplejson.dumps({'url': _.app_link, 'id': _.id, 'email': email})})return data_listdef execute(self):data_list = []try:data_list = self._execute_with_exception()except Exception as e:LOG_ERROR('app link error {0}'.format(e))return JsonFuncResponser({'data': data_list})
? ? ? ? 在原代碼中Db對象是可被重用的,而修改后我們需要在不同成員函數中使用到它,所以將其提升成成員變量。
? ? ? ? 沒有在構造函數中直接構造Db對象,是因為希望構造函數足夠簡單,只是進行一些數值型的構造,而不發生諸如“連接數據庫”這類比較重的操作。
? ? ? ? 這樣為了不頻繁構建DB對象,我們設計了_init_db方法,同時在使用Db的地方都用其初始化一下。
? ? ? ? 我們修復了原代碼中對user結構的“預設”隱患(直接取用了user[0[0]),同時也給我們暴露出“如果email為空該怎么辦?”業務相關的問題。于是我們引入_email_empty方法來處理該業務性問題。
? ? ? ? 最后我們將execute封裝出一個拋出異常的版本和無異常的版本。
? ? ? ? 經過改造后,代碼結構變得清晰,execute函數職責也變得清晰。
? ? ? ? 分析這段代碼,我們可以列出大致的測試點:
-
_get_all_valid_applinks/_get_email_by_user_id拋出異常
-
_get_all_valid_applinks/_get_email_by_user_id返回None
-
_get_all_valid_applinks返回空List
-
_get_all_valid_applinks返回的不是List
? ? ? ? 明確好這些測試點,我們開始編寫單元測試代碼
監測拋出異常
? ? ? ? 我們使用mock技術,在第9、10和21、22分別讓,分別讓執行_get_all_valid_applinks、_get_email_by_user_id時拋出異常
class TestCheckLinkRequest():def setup_class(self):passdef teardown_class(self):passdef test_get_all_valid_applinks_raise_exception(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', side_effect=Exception)t = CheckLinkRequest()with pytest.raises(Exception):t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))def test_get_email_by_user_id_raise_exception(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_email_by_user_id', side_effect=Exception)t = CheckLinkRequest()with pytest.raises(Exception):t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))
? ? ? ? 然后在14、15和26、27行監測調用_execute_with_exception時會拋出異常。
? ? ? ? 最后17和29行執行無拋出異常版本的execute,并在之后判斷返回值是否符合預期。
監測返回None
? ? ? ? 我們先看_get_all_valid_applinks在返回None時的單元測試。
def test_get_all_valid_applinks_return_none(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value=None)t = CheckLinkRequest()with pytest.raises(Exception):t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))
? ? ? ? 我們在2、3行讓_get_all_valid_applinks返回None。由于遍歷None會拋出異常,所以7、8行將監測異常拋出。其他監測和之前相同。
? ? ? ??_get_email_by_user_id返回None的話,它不會拋出異常,所以我們直接調用了_execute_with_exception而不期待其異常。由于email是空,將會觸發_email_empty執行,于是我們在第5行mock了一下該對象的該函數,然后在第11行確定該函數被調用了。
def test_get_email_by_user_id_return_none(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_email_by_user_id', return_value=None)t = CheckLinkRequest()mocker_email_empty = mocker.patch.object(t, '_email_empty')t._execute_with_exception()r = t.execute()assert(False == r.is_same_data(JsonFuncResponser({'data': []})))assert(mocker_email_empty.called)
返回空List/dict
? ? ? ??_get_all_valid_applinks返回空List或者dict,其返回值結果集也將是空。
def test_get_all_valid_applinks_return_empty(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value=[])t = CheckLinkRequest()t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))def test_get_all_valid_applinks_return_obj(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value={})t = CheckLinkRequest()t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))
? ? ? ? 最后我們監測一個正常的情況
def test_result(self, mocker):ret = [{'url': "www.1.com", 'id': 1, 'email': "1@1.com"},{'url': "www.2.com", 'id': 2, 'email': ""}]app_links = []for _ in ret:app_links.append(AppLinks(app_link = _["url"], user_id = _["id"], id = _["id"]))mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value=app_links)def mocker_get_email_by_user_id(id):emails = {1: "1@1.com"}if id in emails:return emails[id]else:return ""mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_email_by_user_id', wraps=mocker_get_email_by_user_id)t = CheckLinkRequest()mocker_email_empty = mocker.patch.object(t, '_email_empty')t._execute_with_exception()r = t.execute()r_list = []for _ in r.json()['data']:r_list.append(simplejson.loads(_["source"]))assert(r_list == ret)assert(mocker_email_empty.call_count == 2)
? ? ? ? 這段代碼我們使用mocker_get_email_by_user_id替換了CheckLinkRequest的_get_email_by_user_id,從而我們可以干涉其內部執行。這也是一種非常常用的設計。
總結
以上是生活随笔為你收集整理的谈一次单元测试驱动代码重构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AWS攻略——使用CodeBuild进行
- 下一篇: 英文简称释义