บ่อยกว่านั้นซอฟต์แวร์ที่เราเขียนโต้ตอบโดยตรงกับสิ่งที่เราจะระบุว่าเป็นบริการ 'สกปรก' ในแง่ของคนธรรมดา: บริการที่มีความสำคัญต่อแอปพลิเคชันของเรา แต่การโต้ตอบมีเจตนา แต่ผลข้างเคียงที่ไม่ต้องการนั่นคือไม่เป็นที่ต้องการในบริบทของการทดสอบอัตโนมัติ
ตัวอย่างเช่นบางทีเรากำลังเขียนแอปโซเชียลและต้องการทดสอบฟีเจอร์ 'โพสต์ไปที่ Facebook' ใหม่ แต่ไม่ต้องการ จริง โพสต์ไปที่ Facebook ทุกครั้งที่เราเรียกใช้ชุดทดสอบของเรา
งูหลาม unittest
ไลบรารีประกอบด้วยแพ็กเกจย่อยชื่อ unittest.mock
- หรือหากคุณประกาศว่าเป็นการพึ่งพาเพียงแค่ mock
- ซึ่งให้วิธีการที่มีประสิทธิภาพและมีประโยชน์อย่างมากในการล้อเลียนและขจัดผลข้างเคียงที่ไม่ต้องการเหล่านี้
หมายเหตุ: mock
คือ รวมใหม่ ในไลบรารีมาตรฐานของ Python 3.3; การแจกแจงก่อนหน้านี้จะต้องใช้ไลบรารีจำลองที่ดาวน์โหลดได้ทาง PyPI .
เพื่อเป็นตัวอย่างอื่นให้คุณและอีกตัวอย่างหนึ่งที่เราจะดำเนินการกับส่วนที่เหลือของบทความโปรดพิจารณา การโทรของระบบ . ไม่ยากที่จะเห็นว่าสิ่งเหล่านี้เป็นตัวเลือกหลักสำหรับการล้อเลียน: ไม่ว่าคุณจะเขียนสคริปต์เพื่อนำไดรฟ์ซีดีออกเว็บเซิร์ฟเวอร์ที่ลบไฟล์แคชเก่าออกจาก /tmp
หรือเซิร์ฟเวอร์ซ็อกเก็ตที่ผูกกับ TCP พอร์ตสิ่งเหล่านี้เรียกคุณลักษณะทั้งหมดที่เป็นผลข้างเคียงที่ไม่ต้องการในบริบทของการทดสอบหน่วยของคุณ
ในฐานะนักพัฒนาคุณสนใจมากขึ้นว่าไลบรารีของคุณเรียกฟังก์ชันระบบสำหรับการดีดซีดีได้สำเร็จ (ด้วยอาร์กิวเมนต์ที่ถูกต้อง ฯลฯ ) ในทางตรงกันข้ามกับการเปิดถาดซีดีของคุณทุกครั้งที่มีการทดสอบ (หรือแย่กว่านั้นคือหลายครั้งเนื่องจากการทดสอบหลายครั้งอ้างอิงรหัสนำออกในระหว่างการทดสอบหน่วยเดียว!)
การโจมตีทางเว็บประเภทใดที่ใช้ฟังก์ชันรับและโพสต์ของรูปแบบ html
ในทำนองเดียวกันการทำให้การทดสอบหน่วยของคุณมีประสิทธิภาพและมีประสิทธิภาพหมายถึงการรักษา 'โค้ดที่ช้า' ไว้ให้มากที่สุดจากการทดสอบอัตโนมัตินั่นคือระบบไฟล์และการเข้าถึงเครือข่าย
สำหรับตัวอย่างแรกของเราเราจะปรับโครงสร้างกรณีทดสอบ Python มาตรฐานจากรูปแบบเดิมไปเป็นแบบเดิมโดยใช้ mock
เราจะแสดงให้เห็นว่าการเขียนกรณีทดสอบด้วยหุ่นจำลองจะทำให้การทดสอบของเราฉลาดขึ้นเร็วขึ้นและสามารถเปิดเผยเพิ่มเติมเกี่ยวกับวิธีการทำงานของซอฟต์แวร์ได้อย่างไร
เราทุกคนจำเป็นต้องลบไฟล์ออกจากระบบไฟล์ของเราเป็นครั้งคราวดังนั้นเรามาเขียนฟังก์ชันใน Python ซึ่งจะทำให้สคริปต์ของเราทำได้ง่ายขึ้นเล็กน้อย
#!/usr/bin/env python # -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename)
เห็นได้ชัดว่า rm
ของเรา วิธีการ ณ เวลานี้ไม่ได้ให้อะไรมากไปกว่าพื้นฐาน os.remove
วิธีการ แต่ codebase ของเราจะปรับปรุงทำให้เราสามารถเพิ่มฟังก์ชันการทำงานได้มากขึ้นที่นี่
เรามาเขียนกรณีทดสอบแบบเดิม ๆ นั่นคือไม่มีล้อเลียน:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import os.path import tempfile import unittest class RmTestCase(unittest.TestCase): tmpfilepath = os.path.join(tempfile.gettempdir(), 'tmp-testfile') def setUp(self): with open(self.tmpfilepath, 'wb') as f: f.write('Delete me!') def test_rm(self): # remove the file rm(self.tmpfilepath) # test that it was actually removed self.assertFalse(os.path.isfile(self.tmpfilepath), 'Failed to remove the file.')
กรณีทดสอบของเราค่อนข้างง่าย แต่ทุกครั้งที่เรียกใช้ไฟล์ชั่วคราวจะถูกสร้างขึ้นแล้วลบออก นอกจากนี้เราไม่มีวิธีทดสอบว่า rm
ของเราหรือไม่ วิธีการส่งผ่านอาร์กิวเมนต์อย่างถูกต้องไปที่ os.remove
โทร. เราทำได้ สมมติ เป็นไปตามการทดสอบข้างต้น แต่ยังเหลืออีกมากที่ต้องการ
มาปรับโครงสร้างกรณีทดสอบของเราใหม่โดยใช้ mock
:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm('any path') # test that rm called os.remove with the right parameters mock_os.remove.assert_called_with('any path')
ด้วย refactors เหล่านี้เราได้เปลี่ยนวิธีการดำเนินการทดสอบโดยพื้นฐานแล้ว ตอนนี้เรามีไฟล์ คนวงใน ซึ่งเป็นวัตถุที่เราสามารถใช้เพื่อตรวจสอบการทำงานของวัตถุอื่นได้
สิ่งแรกที่ควรคำนึงถึงคือเรากำลังใช้ mock.patch
เมธอดมัณฑนากรเพื่อจำลองวัตถุที่อยู่ที่ mymodule.os
และฉีดจำลองนั้นลงในวิธีกรณีทดสอบของเรา คงไม่สมเหตุสมผลไปกว่าการล้อเลียน os
เองแทนที่จะอ้างอิงที่ mymodule.os
?
Python ค่อนข้างเป็นงูที่ส่อเสียดในการนำเข้าและจัดการโมดูล ที่รันไทม์ mymodule
โมดูลมีของตัวเอง os
ซึ่งนำเข้าสู่ขอบเขตโลคัลของตนเองในโมดูล ดังนั้นหากเราล้อเลียน os
เราจะไม่เห็นผลกระทบของการล้อเลียนใน mymodule
โมดูล.
มนต์ที่ต้องทำซ้ำ ๆ คือ:
เลียนแบบสิ่งของที่ใช้ไม่ใช่ที่มา
หากคุณต้องการล้อเลียน tempfile
โมดูลสำหรับ myproject.app.MyElaborateClass
คุณอาจต้องใช้การจำลองกับ myproject.app.tempfile
เนื่องจากแต่ละโมดูลเก็บการนำเข้าของตัวเอง
ด้วยความผิดพลาดนั้นเรามาล้อเลียนกันต่อไป
rm
วิธีการที่กำหนดไว้ก่อนหน้านี้ค่อนข้างง่ายเกินไป เราต้องการให้ตรวจสอบความถูกต้องว่ามีเส้นทางและเป็นไฟล์ก่อนที่จะพยายามลบออกโดยสุ่มสี่สุ่มห้า ขอ refactor rm
ฉลาดขึ้นหน่อย:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path def rm(filename): if os.path.isfile(filename): os.remove(filename)
เยี่ยมมาก ตอนนี้เรามาปรับเปลี่ยนกรณีทดสอบของเราเพื่อให้ครอบคลุม
ไฟล์ .cc เทียบกับ .cpp
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # set up the mock mock_path.isfile.return_value = False rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True rm('any path') mock_os.remove.assert_called_with('any path')
กระบวนทัศน์การทดสอบของเราเปลี่ยนไปอย่างสิ้นเชิง ตอนนี้เราสามารถตรวจสอบและตรวจสอบการทำงานภายในของวิธีการโดยไม่ต้อง ใด ๆ ผลข้างเคียง.
จนถึงขณะนี้เราดำเนินการเฉพาะกับการจัดหา mocks สำหรับฟังก์ชันเท่านั้น แต่ยังไม่ใช้สำหรับวิธีการกับวัตถุหรือกรณีที่จำเป็นต้องมีการจำลองเพื่อส่งพารามิเตอร์ เรามาดูวิธีการของวัตถุกันก่อน
เราจะเริ่มต้นด้วย refactor ของ rm
วิธีการลงในคลาสบริการ ไม่มีความจำเป็นที่สมเหตุสมผลในการห่อหุ้มฟังก์ชันง่ายๆเช่นนี้ไว้ในวัตถุ แต่อย่างน้อยที่สุดก็จะช่วยให้เราแสดงแนวคิดหลักใน mock
มาดูกันว่า
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): '''A service for removing objects from the filesystem.''' def rm(filename): if os.path.isfile(filename): os.remove(filename)
คุณจะสังเกตเห็นว่าไม่มีการเปลี่ยนแปลงมากนักในกรณีทดสอบของเรา:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True reference.rm('any path') mock_os.remove.assert_called_with('any path')
เยี่ยมมากตอนนี้เรารู้แล้วว่า RemovalService
ทำงานได้ตามแผน มาสร้างบริการอื่นที่ประกาศว่าเป็นการพึ่งพา:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): '''A service for removing objects from the filesystem.''' def rm(self, filename): if os.path.isfile(filename): os.remove(filename) class UploadService(object): def __init__(self, removal_service): self.removal_service = removal_service def upload_complete(self, filename): self.removal_service.rm(filename)
เนื่องจากเราได้ครอบคลุมการทดสอบใน RemovalService
แล้วเราจะไม่ตรวจสอบความถูกต้องของฟังก์ชันการทำงานภายในของ rm
วิธีการในการทดสอบ UploadService
ของเรา แต่เราจะทดสอบ (โดยไม่มีผลข้างเคียงแน่นอน) ว่า UploadService
โทร ที่ RemovalService.rm
ซึ่งเรารู้ว่า 'ใช้ได้ผล' จากกรณีทดสอบก่อนหน้านี้
มีสองวิธีในการดำเนินการนี้:
RemovalService.rm
วิธีการเองUploadService
เนื่องจากทั้งสองวิธีมักมีความสำคัญในการทดสอบหน่วยเราจะตรวจสอบทั้งสองอย่าง
mock
ไลบรารีมีตัวตกแต่งเมธอดพิเศษสำหรับการเยาะเย้ยเมธอดและคุณสมบัติของออบเจ็กต์เช่น @mock.patch.object
มัณฑนากร:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True reference.rm('any path') mock_os.remove.assert_called_with('any path') class UploadServiceTestCase(unittest.TestCase): @mock.patch.object(RemovalService, 'rm') def test_upload_complete(self, mock_rm): # build our dependencies removal_service = RemovalService() reference = UploadService(removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete('my uploaded file') # check that it called the rm method of any RemovalService mock_rm.assert_called_with('my uploaded file') # check that it called the rm method of _our_ removal_service removal_service.rm.assert_called_with('my uploaded file')
เยี่ยมมาก! เราได้ตรวจสอบแล้วว่า UploadService
เรียกอินสแตนซ์ของเราสำเร็จ rm
วิธี. สังเกตเห็นอะไรที่น่าสนใจในนั้น? กลไกการแก้ไขแทนที่ rm
วิธีการทั้งหมด RemovalService
อินสแตนซ์ในวิธีการทดสอบของเรา นั่นหมายความว่าเราสามารถตรวจสอบอินสแตนซ์ได้ด้วยตนเอง หากคุณต้องการดูข้อมูลเพิ่มเติมให้ลองวางเบรกพอยต์ในโค้ดจำลองของคุณเพื่อรับความรู้สึกที่ดีว่ากลไกการแก้ไขทำงานอย่างไร
เมื่อใช้นักตกแต่งหลายคนในวิธีการทดสอบของคุณ คำสั่งซื้อเป็นสิ่งสำคัญ และเป็นเรื่องที่น่าสับสน โดยทั่วไปเมื่อแมปมัณฑนากรกับพารามิเตอร์วิธีการ ทำงานย้อนหลัง . ลองพิจารณาตัวอย่างนี้:
@mock.patch('mymodule.sys') @mock.patch('mymodule.os') @mock.patch('mymodule.os.path') def test_something(self, mock_os_path, mock_os, mock_sys): pass
สังเกตว่าพารามิเตอร์ของเราตรงกับลำดับย้อนกลับของมัณฑนากรหรือไม่? ส่วนหนึ่งเป็นเพราะ วิธีการทำงานของ Python . เมื่อใช้มัณฑนากรหลายวิธีนี่คือลำดับของการดำเนินการในรหัสเทียม:
patch_sys(patch_os(patch_os_path(test_something)))
ตั้งแต่แพทช์จนถึง sys
เป็นโปรแกรมแก้ไขที่อยู่นอกสุดซึ่งจะถูกเรียกใช้งานครั้งสุดท้ายทำให้เป็นพารามิเตอร์สุดท้ายในอาร์กิวเมนต์ของวิธีการทดสอบจริง จดสิ่งนี้ไว้และใช้ดีบักเกอร์เมื่อทำการทดสอบเพื่อให้แน่ใจว่าพารามิเตอร์ที่ถูกต้องถูกฉีดเข้าไปในลำดับที่ถูกต้อง
แทนที่จะล้อเลียนวิธีการอินสแตนซ์ที่เฉพาะเจาะจงเราสามารถจัดหาอินสแตนซ์จำลองให้กับ UploadService
แทนได้ ด้วยตัวสร้าง ฉันชอบตัวเลือกที่ 1 ข้างต้นมากกว่าเนื่องจากมีความแม่นยำกว่ามาก แต่มีหลายกรณีที่ตัวเลือก 2 อาจมีประสิทธิภาพหรือจำเป็น มาปรับเปลี่ยนการทดสอบของเราอีกครั้ง:
แอพหาคู่ยอดนิยมประจำปี 2017
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True reference.rm('any path') mock_os.remove.assert_called_with('any path') class UploadServiceTestCase(unittest.TestCase): def test_upload_complete(self, mock_rm): # build our dependencies mock_removal_service = mock.create_autospec(RemovalService) reference = UploadService(mock_removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete('my uploaded file') # test that it called the rm method mock_removal_service.rm.assert_called_with('my uploaded file')
ในตัวอย่างนี้เราไม่จำเป็นต้องแก้ไขการทำงานใด ๆ ด้วยซ้ำเราเพียงแค่สร้างข้อมูลจำเพาะอัตโนมัติสำหรับ RemovalService
จากนั้นฉีดอินสแตนซ์นี้ลงใน UploadService
ของเรา เพื่อตรวจสอบฟังก์ชันการทำงาน
mock.create_autospec
เมธอดสร้างอินสแตนซ์ที่เทียบเท่ากับฟังก์ชันให้กับคลาสที่ให้มา สิ่งนี้หมายถึงในทางปฏิบัติคือเมื่ออินสแตนซ์ที่ส่งคืนมีการโต้ตอบมันจะเพิ่มข้อยกเว้นหากใช้ในรูปแบบที่ผิดกฎหมาย โดยเฉพาะอย่างยิ่งถ้าเมธอดถูกเรียกด้วยจำนวนอาร์กิวเมนต์ที่ไม่ถูกต้องจะมีการเพิ่มข้อยกเว้น สิ่งนี้มีความสำคัญอย่างยิ่งเมื่อ refactors เกิดขึ้น เมื่อมีการเปลี่ยนแปลงไลบรารีการทดสอบจะแตกและคาดว่าจะเกิดขึ้น หากไม่ใช้ข้อมูลจำเพาะอัตโนมัติการทดสอบของเราจะยังคงผ่านแม้ว่าการใช้งานพื้นฐานจะเสีย
mock.Mock
และ mock.MagicMock
ชั้นเรียนmock
ไลบรารียังมีคลาสที่สำคัญอีกสองคลาสซึ่งฟังก์ชันภายในส่วนใหญ่สร้างขึ้นจาก: mock.Mock
และ mock.MagicMock
. เมื่อมีตัวเลือกให้ใช้ mock.Mock
เช่น a mock.MagicMock
อินสแตนซ์หรือข้อมูลจำเพาะอัตโนมัติมักชอบใช้ข้อมูลจำเพาะอัตโนมัติเนื่องจากจะช่วยให้การทดสอบของคุณมีเหตุผลสำหรับการเปลี่ยนแปลงในอนาคต เนื่องจาก mock.Mock
และ mock.MagicMock
ยอมรับการเรียกเมธอดและการกำหนดคุณสมบัติทั้งหมดโดยไม่คำนึงถึง API ที่อยู่เบื้องหลัง พิจารณากรณีการใช้งานต่อไปนี้:
class Target(object): def apply(value): return value def method(target, value): return target.apply(value)
เราสามารถทดสอบได้ด้วย mock.Mock
อินสแตนซ์เช่นนี้:
class MethodTestCase(unittest.TestCase): def test_method(self): target = mock.Mock() method(target, 'value') target.apply.assert_called_with('value')
ตรรกะนี้ดูเหมือนจะมีเหตุผล แต่เรามาแก้ไข Target.apply
กัน วิธีการใช้พารามิเตอร์เพิ่มเติม:
class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None
ทำการทดสอบอีกครั้งและคุณจะพบว่ายังผ่าน นั่นเป็นเพราะไม่ได้สร้างขึ้นจาก API จริงของคุณ นี่คือเหตุผลที่คุณควร เสมอ ใช้ create_autospec
วิธีการและ autospec
ด้วยพารามิเตอร์ @patch
และ @patch.object
มัณฑนากร.
เพื่อให้เสร็จสมบูรณ์เรามาเขียนตัวอย่างการจำลองงูหลามในโลกแห่งความเป็นจริงที่เกี่ยวข้องมากขึ้นซึ่งเราได้กล่าวถึงในบทนำนั่นคือการโพสต์ข้อความไปยัง Facebook เราจะเขียนคลาส Wrapper ที่สวยงามและกรณีทดสอบที่เกี่ยวข้อง
import facebook class SimpleFacebook(object): def __init__(self, oauth_token): self.graph = facebook.GraphAPI(oauth_token) def post_message(self, message): '''Posts a message to the Facebook wall.''' self.graph.put_object('me', 'feed', message=message)
นี่คือกรณีทดสอบของเราซึ่งตรวจสอบว่าเราโพสต์ข้อความโดยไม่มี จริง โพสต์ข้อความ:
import facebook import simple_facebook import mock import unittest class SimpleFacebookTestCase(unittest.TestCase): @mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True) def test_post_message(self, mock_put_object): sf = simple_facebook.SimpleFacebook('fake oauth token') sf.post_message('Hello World!') # verify mock_put_object.assert_called_with(message='Hello World!')
อย่างที่เราเห็นมาจนถึงตอนนี้ จริงๆ ง่ายต่อการเริ่มเขียนแบบทดสอบอย่างชาญฉลาดด้วย mock
ใน Python
Python ของ mock
หากมีความสับสนเล็กน้อยในการใช้งานเป็นตัวเปลี่ยนเกมสำหรับ การทดสอบหน่วย . เราได้สาธิตกรณีการใช้งานทั่วไปสำหรับการเริ่มต้นใช้งาน mock
ในการทดสอบหน่วยและหวังว่าบทความนี้จะช่วยได้ นักพัฒนา Python เอาชนะอุปสรรคเริ่มต้นและเขียนโค้ดที่ดีเยี่ยมและผ่านการทดสอบแล้ว
การล้อเลียนจำลองการมีอยู่และพฤติกรรมของวัตถุจริงทำให้วิศวกรซอฟต์แวร์สามารถทดสอบโค้ดในสถานการณ์สมมติต่างๆได้โดยไม่จำเป็นต้องใช้การเรียกระบบจำนวนนับไม่ถ้วน การล้อเลียนจะช่วยเพิ่มความเร็วและประสิทธิภาพของการทดสอบหน่วยได้อย่างมาก
การจำลองใน Python หมายถึงไลบรารี unittest.mock ถูกนำมาใช้เพื่อแทนที่ส่วนต่างๆของระบบด้วยอ็อบเจ็กต์จำลองทำให้การทดสอบหน่วยง่ายขึ้นและมีประสิทธิภาพมากกว่าที่จะเป็นไปได้