使用 Vagrant 和 Fabric 用于集成測試
在cloudshare中,我們的服務是由許多部件組成的。當我們更改一個給定組件的代碼后我們總需要測試它。我們小心地嘗試著平衡單元測試和集成測試(或系統測試)的總量,以便能夠實現合理的代碼覆蓋率和測試運行時間,最重要的是提升對我們代碼的信心。
不久前,我們徹底改寫了一個叫網關的組件。這個網關運行在Linux機器上,其處理了我們內部的許多路由,防火墻,NAT,負載平衡以及流量日志等很多內容。它基本上是一個路由器/防火墻,通過獲取動態配置并根據它了解的配置實施不同的網絡規則。這次改寫是通過重新設計其(虛擬)硬件和內核模塊完成的。它是一個Python應用包使用原始的debian打包部署的。
在重寫之前,這個網關是“冰封”的。“冰封”在這里的意思是沒有人敢修改它的代碼。它沒有測試代碼,因此每個更改都需要一份完整的手冊,單是痛苦的回歸測試也需要花一個星期。
我們坐下來定義了我們的目標。我們希望所有的開發人員都能夠在本地的機器跑所有的集成測試,并且能夠很容易。很容易還意味著在變更代碼后不需要部署其他任何東西。需要做的這是在IDE中編輯代碼然后重新運行測試。不需要提交代碼,不需要重新打包,不需要部署(我們在Windows上開發)/
當進行測試時就不是那么容易了,你知道會發生什么。
改善集成測試:
我們已經知道需要改善我們的單元測試。但是集成測試呢?那是另一回事。你如何測試你的硬件和內核配置以確保這些配置能完成你所想讓它實現的網絡魔術。
讓我們考慮如何手動來做這個事情。簡單的方法是用linux提供的一系列網絡工具:ping,traceroute,tcpdump,netcat等。事實上,這也正是我們QA工程師做的事情:
部署新代碼.
創建一個由幾臺連接到同一個網關的機器組成的測試平臺。
對于任何可能的配置,網關都會測試整個網絡的功能應該是流暢/阻塞/跑通NAT/路由等。
這簡直是一場噩夢。
我甚至敢說:不要QA工程師,就算他們可能才華橫溢,不留死角地涵蓋所有情況,或浪費時間(合理數量)。 更何況我們希望的是,他們比只是會弄回歸測試多一點創意。 這實際上是機器的工作,而不是人的工作。
我們終于面臨了嚴酷考驗,并開始思考為何這是可能的。 Vagrant,那時完全是個新的后備方案,來的如此自然。它允許我們能夠創建一個由不同的虛擬局域網連接的虛擬機的環境。 Vagrant 還可以讓你直接掛載你在主機文件夾到你管理的虛擬機,并且也滿足我們的“容易測試”的要求。 如果代碼已經被掛載在VM Vagrant,沒必要進行部署。
下面是vagrant 文件, 來定義虛擬環境:
Vagrant::Config.run do |config| config.vm.define :gateway do |gateway_config| gateway_config.vm.box = "gateway" gateway_config.vm.host_name = "gateway" gateway_config.vm.box_url = "http://FQDN…./gateway.box"gateway_config.vm.network :hostonly, "192.168.58.2", { :adapter => 2, :netmask => '255.255.255.0' } gateway_config.vm.network :hostonly, "192.168.56.90", {:adapter => 3, :auto_config => false} gateway_config.vm.share_folder "code", "/code", "../../..", :mount_options => ["dmode=755", "fmode=755"] end
config.vm.define :tester1 do |config| config.vm.box = "tester" config.vm.host_name = "tester1" config.vm.box_url = "http://FQDN…../tester.box"
config.vm.network :hostonly, "192.168.58.91", {:adapter => 2, :netmask => '255.255.255.0' } config.vm.network :hostonly, "192.168.56.91", {:adapter => 3, :auto_config => false}
config.vm.share_folder "code", "/code", "../tests" end
config.vm.define :tester2 do |config| config.vm.box = "tester" config.vm.host_name = "tester2" config.vm.box_url = "http://FQDN…./tester.box"
config.vm.network :hostonly, "192.168.58.92", {:adapter => 2, :netmask => '255.255.255.0' } config.vm.network :hostonly, "192.168.56.91", {:adapter => 3, :auto_config => false}
config.vm.share_folder "code", "/code", "../tests" end … more testers machines defined here ...</pre>
如你所見,本地源碼唄掛載/編寫在vagrant虛擬機中。在這也有網絡定義。一個作為集成測試的物理網絡用來配置VLANs(注意:auto_confi => false option)和其他用來測試代碼通信。
當開發者運行一段測試時發生了什么?
實際上是在網關虛擬機上運行了測試。使用了本地掛載代碼來創建應用對象,調用對象,然后使用 fabric在測試機器上遠程運行網絡工具來ping/sniff/trace/accept 所有通過和返回給網關的流量的種類。
下面來看個簡單的例子,簡化了很多的:
class TestVlansBase(unittest.TestCase): def setUp(self): self._initialize_tester_machines()def tearDown(self): for tester_name, vlan in self.dct_interfaces_to_remove.iteritems(): self._remove_interface_from_host(tester_name, vlan)
class TestVlans(TestVlansBase): def _test_connection( self, server_name, server_vlan, server_ip, server_port, protocol, client_name, client_dst_ip=None, client_dst_port=None): self.assertTrue(protocol in ('tcp', 'udp'), 'protocol should be tcp or udp') client_dst_ip = client_dst_ip or server_ip client_dst_port = client_dst_port or server_port if protocol == 'tcp': server = self._create_server(server_name, server_ip, server_port) elif protocol == 'udp': filter_exp = '{0} port {1}'.format(protocol, server_port) server = self._create_sniffer_on_host(server_name, server_vlan, filter_exp, 1) client = self._connect_to_host(client_name, client_dst_ip, client_dst_port, protocol) client.runner.join() if server.runner.exitcode is None: # this means that the process did not exit hence no packets were seen server.runner.terminate() server.runner.join() self.assertEqual(client.runner.exitcode, 0) self.assertEqual(server.runner.exitcode, 0)
def test_reroute_http_traffic(self): self._configure_testers() self.gateway.configure() self._test_connection( 'tester3', 93, '10.180.0.3', 88, 'tcp', 'tester2', client_dst_ip='10.10.10.10', client_dst_port=80) self._test_connection( 'tester3', 93, '10.180.0.3', 88, 'tcp', 'tester2', client_dst_ip='10.10.10.10', client_dst_port=44444) self._test_connection( 'tester3', 93, '10.180.0.3', 88, 'tcp', 'tester2', client_dst_ip='10.10.10.10', client_dst_port=88)</pre>
所有的從網關(測試運行的地方)到測試者機器的遠程調用使用的是fabric。
一個可以在測試上運行的簡單命令:
class FabricProcessProxy(object): metaclass = ABCMetadef init(self, args, **kwargs): self.kwargs = kwargs self.args = args self.out_q = multiprocessing.Queue() # self.runner = multiprocessing.Process(target=lambda: execute(self.run, self.args, self.kwargs)) self.runner = multiprocessing.Process( target=lambda: self.out_q.put(execute(self.run, *self.args, self.kwargs)))
def execute(self, hosts): self.kwargs['hosts'] = hosts self.runner.start() return self.runner
@abstractmethod def run(self): raise NotImplementedError()
class Ping(FabricProcessProxy): def run(self, target, iface, count): str_iface = '-I {0} '.format(iface) if iface else ' ' return run('ping -c {count}{iface}-W 1 {target}'.format(count=count, iface=str_iface, target=target))
def _ping_from_host(self, host, dst_ip, through_iface=None, num_pings=1, b_verify_success=True): ping = Ping(dst_ip, through_iface, num_pings) ping.execute(['vagrant@{0}'.format(host)]).join() if b_verify_success: self.assertEqual(ping.runner.exitcode, 0) return ping.runner.exitcode
_ping_from_host('tester2', '10.180.0.3')</pre>
既然這個基礎結構已經建好了,我們就不在回頭看它了。我們今天所擁有的網關是一等公民,而且只要通過了測試,我們就不怕重構它,添加新的功能和做出其他改變。
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!