iT邦幫忙

2

如何使用unittest.mock測試多個python的 with open

  • 分享至 

  • xImage

各位大神好,本人想問一個關於Python3的unittest.mock的問題

我在mainFunction.py的main()中用with.open分別寫入file1以及讀取file2兩個檔
亦即mainFunction.py的main()當中有以下code:

with open("path/to/file1", "w+") as f1:
    f1.write("message for file1")
with open("path/to/file2", "rb") as f2:
    print(f2.read())
sys.exit(0)

以下為我的unit test code test_main.py

import mainFunction
from unittest import mock

class MyUnitTest(unittest.TestCase):
    def test_main(self):
        with self.assertRaises(SystemExit) as cm, mock.patch("builtins.open",
        side_effect = [
                       mock.mock_open(read_data="").return_value,
                       mock.mock_open(read_data="read from file2").return_value
                      ]) as mock_files:
            mainFunction.main()
            mock_files.return_value.__enter__().write.assert_called_once_with("message for file1")
        mock_files.return_value.__enter__().read.assert_called_once_with("message for
file2")
        self.assertEqual(cm.exception, 0)   

經過我的測試"read from file2"是真的有被with open("path/to/f2", "r") as f2 讀取,f2.read()印出來就是"read from file2"

然而 mock_files.return_value.enter() 那幾行皆會出現 Assertion Error:

AssertionError: Expect 'read' to be called once. Called 0 times
AssertionError: Expect 'write' to be called once. Called 0 times

求解如何使用mock_files來驗證傳送至file1的"message for file1",還有寫入file2的訊息是否正確,本人爬了很多stackoverflow的文都還找不到解,感謝!

圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 個回答

0
re.Zero
iT邦研究生 5 級 ‧ 2023-01-21 14:23:28
最佳解答

先上改自你程式碼的我版:
(大致上說明內容都丟註解內了~)

## mainFunction.py
import sys
def main():
	with open("path/to/file1", "w+") as f1:
		f1.write("message for file1")
	with open("path/to/file2", "rb") as f2:
		print(f2.read())
	sys.exit(0)
## 
if __name__ == '__main__': main()
## 
## test_main.py
import unittest
from unittest import mock
import mainFunction
## 
class MyUnitTest(unittest.TestCase):
	def test_main(self):
		## 這個 tuple 是因為我需要 MagicMock (created by mock_open()) 的參照(obj-ref);
		myHandles = (
			mock.mock_open(),
			mock.mock_open(read_data="read from file2")
		)
		## https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.side_effect
		with \
			self.assertRaises(SystemExit) as cm, \
			mock.patch(
				"builtins.open", 
				side_effect = tuple([
					i.return_value for i in myHandles
				])
			) as mock_files, \
			mock.patch("builtins.print") as mock_print \
		:
			mainFunction.main()
		## 檢查 mock_calls:這段是讓你知道 [write()]、[read()] 會發生在哪裡;
		for i in mock_files.mock_calls:
			print('■[myUT] mock_files.mock_calls:', i)
		for i in range(len(myHandles)):
			for j in myHandles[i].mock_calls:
				print(f'■[myUT] myHandles[{i}].mock_calls:', j)
		for i in mock_print.mock_calls:
			print('■[myUT] mock_print.mock_calls:', i)
		#mock_files.return_value.__enter__().write.assert_called_once_with("message for file1")
		myStrs = ['message for file1']
		myStrs.append(myStrs[-1] + 'p')
		for i in myStrs:
			try: ## 用 try 是因為我想測試 i (in myStrs) 的值不同時,會不會觸發例外,並能繼續後續流程;
				myHandles[0].return_value .__enter__().write .assert_called_once_with(i)
			except Exception as err:
				print('■ Exception: ',end=''); print(err)
			else : print(f'■ Fine:Str[{i}];')
		#mock_files.return_value.__enter__().read.assert_called_once_with("message for file2")
		## 上面這行我不懂你想做啥;你檢查 read() 所用的參數你不覺得有問題?
		## read() 跟 write() 是不同的動作,參數定義也不同耶……
		## 若想驗證 [f2.read()] (@mainFunction.py),我就用 mock_print 驗證  [print(f2.read())] 了;
		## (雖說,就 mock 的用途而言,我覺得在一般狀況下想去驗證 mock 的 return_value 的想法有點神奇……)
		myStr = 'message for file2'
		try: mock_print.assert_called_once_with(myStr)
		except Exception as err:
			print('■ Exception: ',end=''); print(err)
		else : print(f'■ Fine:Str[{myStr}];')
		#self.assertEqual(cm.exception, 0)
		self.assertEqual(cm.exception.code, 0)
## 
if __name__ == '__main__': unittest.main()
## 

我的輸出(WinCmd):

> python "./test_main.py"
■[myUT] mock_files.mock_calls: call('path/to/file1', 'w+')
■[myUT] mock_files.mock_calls: call('path/to/file2', 'rb')
■[myUT] myHandles[0].mock_calls: call().__enter__()
■[myUT] myHandles[0].mock_calls: call().write('message for file1')
■[myUT] myHandles[0].mock_calls: call().__exit__(None, None, None)
■[myUT] myHandles[1].mock_calls: call().__enter__()
■[myUT] myHandles[1].mock_calls: call().read()
■[myUT] myHandles[1].mock_calls: call().__exit__(None, None, None)
■[myUT] mock_print.mock_calls: call('read from file2')
■ Fine:Str[message for file1];
■ Exception: expected call not found.
Expected: write('message for file1p')
Actual: write('message for file1')
■ Exception: expected call not found.
Expected: print('message for file2')
Actual: print('read from file2')
.
----------------------------------------------------------------------
Ran 1 test in 0.027s

OK

你說:

還有寫入file2的訊息是否正確

你的程式碼哪裡有寫入 file2 ?我只看到 f2.read()
(該不會是勉強算上 mock 的 side_effect?這想法對我來說很詭異耶……)

提醒一下,你的程式碼內,mock_files.return_value.__enter__().write.assert_called_once_with("message for file1") 放在 with 內的話,很可能不會被執行喔(因為 mainFunction.pymain() 內有 sys.exit())。
(如果你是故意的就當我沒說~)

另,請不要像我在 unittest 亂用 try-except (除非你知道你在做啥);畢竟 unittest 就是檢測,一般狀況下有異常就該觸發例外。我只是作範例給你看才這樣用喔!

ffaanngg iT邦新手 5 級 ‧ 2023-01-24 08:31:17 檢舉

謝謝,很完美的回答
mock_files.return_value.enter().write.assert_called_once_with("message for file1")
是我放錯地方了,要放with外才對

我要發表回答

立即登入回答