ruby + phantomjs 自动化测试 - GA
说起测试GA,真是一件枯燥乏味,重复性很高的工作,那么为什么我们不使用自动化测试代替它呢,显然,很多公司的产品迭代太快,ga也变化的比较频繁,但是确保ga工作正常,对于其他部门的工作是有很大帮助的,由于公司对于这块比较注重,而且曾经出现过ga被前端修复bug而影响,所以抽空倒腾了下如何对ga进行自动化测试,由于自身比较习惯使用ruby,所以本帖都是ruby的代码,思路是一样的,喜欢的童鞋可以用其他语言去实现。
首先说说开始考虑的实现方案:
1. 使用selenium+firefox的插件抓取request生成har文件,尝试过后发现不可行,点看此文章 http://www.cnblogs.com/timsheng/p/7209964.html
2. 使用proxy,讲浏览器请求server的request转发到本地,proxy的库有很多,ruby内置的webrick就很好用,但是尝试过后发现依然不行,webrick只能抓取http的request,我们网站是https协议的,抓取不到。
3. 使用evil-proxy, https://github.com/bbtfr/evil-proxy 这个库很强大,可以结合selenium使用,原理是运行时会生成自己的签名文件,然后将生成的签名文件import到浏览器就行了,具体如何操作请参考wiki,但是问题又来了,我们网站https的request都可以抓到,除了google的https request无法抓取,会提示无效签名。
4. ok,这些简单的方式都无法成功抓取ga的request,只能出绝招了,可能大家都知道,phantomjs是一个很强大的工具,它可以结合其他框架做headless网站测试,可以截图,不同于selenium截取当前页面图,它可以截取全屏截图,另外它可以做网页测试,最关键的是它可以进行网络监控。 传送门在此,http://phantomjs.org/
所以我们需要使用的是phantomjs, 去进行页面自动化,并且抓取生成的ga requests,用ruby去分析日志,并且进行校验pageview和event的事件是否触发,参数是否正确
不多说上代码:
首先看一下目录结构
我们先来看一下student_ga.js文件
/** * Wait until the test condition is true or a timeout occurs. Useful for waiting * on a server response or for a ui change (fadeIn, etc.) to occur. * * @param testFx javascript condition that evaluates to a boolean, * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or * as a callback function. * @param onReady what to do when testFx condition is fulfilled, * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or * as a callback function. * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. */ "use strict"; function waitFor(testFx, onReady, timeOutMillis) { var maxtimeOutMillis = timeOutMillis || 8000, //< Default Max Timout is 3s start = new Date().getTime(), condition = false, interval = setInterval(function() { if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { // If not time-out yet and condition not yet fulfilled condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code } else { if(!condition) { // If condition still not fulfilled (timeout but condition is 'false') console.log("'waitFor()' timeout"); phantom.exit(1); } else { // Condition fulfilled (timeout and/or condition is 'true') console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled clearInterval(interval); //< Stop this interval } } }, 250); //< repeat check every 250ms }; // initialise various variables var page = require('webpage').create(), system = require('system'), address; page.viewportSize = { width: 1280, height: 800 }; // how long should we wait for the page to load before we exit // in ms var WAIT_TIME = 30000; // if the page hasn't loaded after this long, something is probably wrong // in ms var MAX_EXECUTION_TIME = 30000; // output error messages var DEBUG = true // a list of regular expressions of resources (urls) to log when we load them var resources_to_log = [ new RegExp('^http(s)?://(www|ssl)\.google-analytics\.com.*'), new RegExp('^http(s)?://stats\.g\.doubleclick\.net.*') ]; // page.settings.resourceTimeout = 10000; // check we have a url, if not exit if (system.args.length === 1) { console.log('Usage: get_ga_resources.js http://www.yoururl.com'); phantom.exit(1); } else { // address is the url passed address = system.args[1]; // create a function that is called every time a resource is requested // http://phantomjs.org/api/webpage/handler/on-resource-requested.html page.onResourceRequested = function (res) { // loop round all our regexs to see if this url matches any of them var length = resources_to_log.length; while(length--) { if (resources_to_log[length].test(res.url)){ // we have a match, log it console.log(res.url); } } }; // if debug is true, log errors, else ignore them page.onError = function(msg, trace){ if (DEBUG) { console.log('ERROR: ' + msg); console.log(trace); } }; // output console message // page.onConsoleMessage = function(msg) { // console.log(msg); // }; // page.onResourceTimeout = function(request) { // console.log(request.errorCode); // console.log(request.errorString); // console.log(request.url); // // console.log('Response (#' + request.id + '): ' + JSON.stringify(request)); // }; // page.onResourceError = function(resourceError) { // console.log('Unable to load resource (#' + resourceError.id + 'URL:' + resourceError.url + ')'); // console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); // }; // now all we have to do is open the page, wait WAIT_TIME ms and exit try { page.open(address, function (status) { console.log("Starting to open --" + address); if (status !== 'success') { console.log("FAILED: to load " + system.args[1]); console.log(page.reason_url); console.log(page.reason); phantom.exit(); } else { // page is loaded! if(address != page.url){ console.log('Redirected: ' + page.url) } // start to do actions on website console.log("Success to open --" + address); page.render("screenshot/homepage.png"); // select top city london to click page.evaluate(function(s) { console.log("Click top city -> London"); document.querySelector(".top-cities__city:nth-child(1)>a>img").click(); console.log("click it done!!!!"); }); // submit enquiry until srp load completely setTimeout(function(){ waitFor(function() { // Check in the page if a specific element is now visible return page.evaluate(function() { console.log("determine if contact an expert element is visible? ") cae_element = document.querySelector(".advicebar__btn.button.button--keppel.js-account-modal-enquire"); if (cae_element.offsetWidth > 0 && cae_element.offsetHeight > 0) { return true; } else { return false; }; }); }, function() { console.log("Page_Url:" + page.url); console.log("--- Get into search result page ---"); page.render("screenshot/search_result_page.png"); page.evaluate(function() { console.log(document.querySelector(".advicebar__btn.button.button--keppel.js-account-modal-enquire").innerHTML); document.querySelector(".advicebar__btn.button.button--keppel.js-account-modal-enquire").click(); console.log("Click contact an expert button"); }); console.log("--- Get into entry screen ---"); page.render("screenshot/entry_screen.png"); page.evaluate(function() { document.querySelector("#js-account-modal-enquiry .account-modal__step--visible .button.button--secondary").click(); console.log("--- Get into sign up screen ---"); document.querySelector("#js-account-modal-enquiry input[name='full_name']").value = 'tim sheng'; document.querySelector("#js-account-modal-enquiry input[name='telephone']").value = '123123123'; document.querySelector("#js-account-modal-enquiry input[name='email']").value = ("tim.sheng+" + (new Date().getTime()) + "@student.com"); document.querySelector("#js-account-modal-enquiry input[name='password']").value = 'abc20052614'; }); page.render("screenshot/sign_up_screen.png"); page.evaluate(function() { console.log('Click sign up button'); document.querySelector("#set-password-button").click(); }); waitFor(function() { return page.evaluate(function() { console.log("determine if confirm button is visible? on about you screen"); confirm_element = document.querySelector("#js-account-modal-enquiry #submit-about-you-button"); if (confirm_element.offsetWidth > 0 && confirm_element.offsetHeight > 0) { return true; } else { return false; }; }); }, function() { console.log("--- Get into about you screen ---"); page.render("screenshot/about_you_screen.png"); page.evaluate(function() { document.querySelector("#js-account-modal-enquiry #submit-about-you-button").click(); }); waitFor(function() { return page.evaluate(function() { console.log("determine if budget field is visible? on about listing screen"); budget_element = document.querySelector("#js-account-modal-enquiry input[name='budget']"); if (budget_element.offsetWidth > 0 && budget_element.offsetHeight > 0) { return true; } else { return false; }; }); }, function() { console.log("--- Get into about listing screen ---"); page.render("screenshot/about_listig_screen_unfilled.png"); page.evaluate(function() { // click date picker plugin document.querySelector("#js-account-modal-enquiry .date-picker").click(); // select move in date document.querySelectorAll("#js-account-modal-enquiry .js-date-picker-move-in-fieldset input[class='js-date-picker-move-in-month']:not(:disabled)+label")[0].click(); // select move out date document.querySelectorAll("#js-account-modal-enquiry .js-date-picker-move-out-fieldset input[class='js-date-picker-move-out-month']:not(:disabled)+label")[0].click(); // input budget value document.querySelector("#js-account-modal-enquiry input[name='budget']").value = '1234'; // input university value document.querySelector("#js-account-modal-enquiry .account-modal__step--visible input[name='university']").value = 'london'; // dispatch inputing event to elem var event = new Event('inputing'); input_elem = document.querySelector("#js-account-modal-enquiry .account-modal__step--visible input[name='university']"); input_elem.focus(); input_elem.dispatchEvent(event); }); waitFor(function() { return page.evaluate(function() { console.log("determine if university is visible? on autocomplete list"); uni_element = document.querySelector('#js-account-modal-enquiry .autocomplete__item:first-child .autocomplete__item__link'); if (uni_element.offsetWidth > 0 && uni_element.offsetHeight > 0) { return true; } else { return false; }; }); }, function() { console.log("--- University is visible on autocomplete list"); page.render("screenshot/about_listing_university.png"); page.evaluate(function() { document.querySelector('#js-account-modal-enquiry .autocomplete__item:first-child .autocomplete__item__link').click(); }); page.render("screenshot/about_listing_screen_filled.png"); page.evaluate(function() { console.log("Click submit enquiry button"); document.querySelector("#js-account-modal-enquiry .account-modal__step--visible #submit-about-stay-button").click(); }); waitFor(function() { return page.evaluate(function() { console.log("determine if success button is visible? on leads process screen"); success_element = document.querySelector("#js-account-modal-enquiry .account-modal__step--visible .button.button--primary"); if (success_element.offsetWidth > 0 && success_element.offsetHeight > 0) { return true; } else { return false; }; }); }, function() { console.log("submit enquiry from srp cae successfully!"); page.render("screenshot/enquiry_success_screen.png"); }); }); }); }); }); },5000); setTimeout(function () { phantom.exit(); }, WAIT_TIME); } }); } finally { // if we are still running after MAX_EXECUTION_TIME ms exit setTimeout(function() { console.log("FAILED: Max execution time " + Math.round(MAX_EXECUTION_TIME) + " seconds exceeded"); phantom.exit(1); }, MAX_EXECUTION_TIME); } }
然后写个ruby类去解析这个log, cop.rb
require 'uri' module Cop class Logger attr_accessor :ga_requests def initialize path @ga_requests = open_log_file path end # fetch pageview ga def pageview Gas.new ga_requests, 'pageview' end # fetch event ga def event Gas.new ga_requests, 'event' end # get all google analytics request records def open_log_file path all_ga_requests = [] File.open("#{path}").each do |line| all_ga_requests << line if line.include? 'google' end all_ga_requests end end # Gas is a class which is consisted of a list of specific ga requests class Gas attr_accessor :gas, :type VALID_KEYS = ['dl','dp','ul','dt','ec','ea','el','cd17','cd15','cd5','cd1','cd14','cg1','cg2','cd18','cd2'] def initialize all_gas, type h_gas = handle_gas all_gas, type @gas = get_expected_gas h_gas @type = type end # return the count of gas def count gas.count end # use the value of key to get corresponding pageview record # it is better for pageview ga using dp to get value # use the value of key to get corresponding event record # it is better for event ga using el to get value def get_gas_by value gas.each do |ga| ga.each do |k,v| if v == value return ga end end end end private # fetch ga requests by type def handle_gas all_gas, type new_gas_arr = [] all_gas.each do |all_ga| if all_ga.include? type decoded_all_ga = URI.decode(all_ga) new_gas_arr << qs_to_hash(decoded_all_ga) end end new_gas_arr end # get expected gas def get_expected_gas gas expected_gas = [] gas.each do |ga| expected_ga = {} ga.each do |k,v| if VALID_KEYS.include? k expected_ga[k] = v else next end end expected_gas << expected_ga end expected_gas end # decode url def qs_to_hash query keyvals = query.split('&').inject({}) do |result, q| k,v = q.split('=') if !v.nil? result.merge({k => v}) elsif !result.key?(k) result.merge({k => true}) else result end end keyvals end end end
Gas类返回的是hash,我希望取hash的value根据object.xx 的形式,而不是hash[] 的方式,所以重新打开hash类,根据ga的常用参数使用define_method动态定义一些方法
hash.rb
class Hash VALID_KEYS = ['dl','dp','ul','dt','ec','ea','el','cd17','cd15','cd5','cd1','cd14','cg1','cg2','cd18','cd2'] def self.ga name define_method "#{name}" do self["#{name}"] end end VALID_KEYS.each do |e| ga "#{e}" end end
基本准备工作差不多了,现在我们用rspec去管理测试用例,在执行case前,我们需要去清洗一下环境,删除log文件,截图,然后执行phantomjs脚本,
bridge.rb
module Cop def clear_env `rm -rf screenshot/*.png` `rm -rf log/*.log` end def submit_lead_from_srp_cae `phantomjs js/student_ga.js https://hurricane-cn.dandythrust.com > log/ga.log` end end
让我们在根目录下rspec --init一下,生成spec_helper.rb 文件,在此文件中,引入cop.rb, hash.rb, bridge.rb以便于在_spec文件中使用
spec_helper.rb
require 'cop' require 'hash' require 'bridge' include Cop
新建个ga_spec.rb文件,开始编写case
require "spec_helper" describe "GA Checking" do describe "New user submit lead from srp cae" do before(:all) do clear_env submit_lead_from_srp_cae end let(:logger) { logger= Cop::Logger.new "log/ga.log" } context "Pageview" do let(:pageview_gas) { logger.pageview } it "should be correct on homepage" do result = pageview_gas.get_gas_by "/" expect(result.dp).to eql "/" expect(result.ul).to eql "en-us" expect(result.cd17).to eql "3rd Party Login" unless result.cd17.nil? expect(result.cd15).to eql "zh-cn" expect(result.cd5).to eql "home" expect(result.cd1).to eql "desktop" expect(result.cd14).to eql "Special Offers" expect(result.cg1).to eql "Home Page" end it "should be correct on search result page" do result = pageview_gas.get_gas_by "/uk/london" expect(result.dp).to eql "/uk/london" expect(result.ul).to eql "en-us" expect(result.cd17).to eql "3rd Party Login" unless result.cd17.nil? expect(result.cd18).to eql "231004024" expect(result.cd15).to eql "zh-cn" expect(result.cd5).to eql "search" expect(result.cd2).to eql "London" expect(result.cd1).to eql "desktop" expect(result.cg1).to eql "Search" expect(result.cg2).to eql "City" end it "should be correct on entry screen" do result = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup" expect(result.ul).to eql "en-us" end it "should be correct on sign up screen" do result = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email" expect(result.ul).to eql "en-us" end it "should be correct on about you screen" do result = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email/confirm_contact_info" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/confirm_contact_info" expect(result.ul).to eql "en-us" end it "should be correct on about stay screen" do result = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email/about_stay" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/about_stay" expect(result.ul).to eql "en-us" end it "should be correct on enquiry success screen" do result = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email/enq_submitted" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/enq_submitted" expect(result.ul).to eql "en-us" end end context "Event" do let(:event_gas) { logger.event } it "click top city on homepage is correct" do result = event_gas.get_gas_by "topCities" expect(result.dp).to eql "/" expect(result.ul).to eql "en-us" expect(result.ea).to eql "topCities" expect(result.ec).to eql "homePage" expect(result.el).to eql "city:231004024" expect(result.cd17).to eql "3rd Party Login" expect(result.cd15).to eql "zh-cn" expect(result.cd5).to eql "home" expect(result.cd1).to eql "desktop" expect(result.cd14).to eql "Special Offers" expect(result.cg1).to eql "Home Page" end it "click contact an expert on srp is correct" do result = event_gas.get_gas_by "cae > button:getInTouch" expect(result.dp).to eql "/uk/london" expect(result.ul).to eql "en-us" expect(result.ea).to eql "cae > button:getInTouch" expect(result.ec).to eql "searchClick" expect(result.el).to eql "231004024-London" expect(result.cd17).to eql "3rd Party Login" expect(result.cd15).to eql "zh-cn" expect(result.cd5).to eql "search" expect(result.cd2).to eql "London" expect(result.cd1).to eql "desktop" expect(result.cg1).to eql "Search" expect(result.cg2).to eql "City" end it "click sign up with email on entry screen is correct" do result = event_gas.get_gas_by "continueWithEmail" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup" expect(result.ul).to eql "en-us" expect(result.ea).to eql "signupScreen" expect(result.ec).to eql "enquiryFlow" expect(result.el).to eql "continueWithEmail" end it "click continue button on sign up screen is correct" do result = event_gas.get_gas_by "continueBtnClicked" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email" expect(result.ul).to eql "en-us" expect(result.ea).to eql "signupWithEmailScreen" expect(result.ec).to eql "enquiryFlow" expect(result.el).to eql "continueBtnClicked" end it "click request details button on about you screen is correct" do result = event_gas.get_gas_by "requestDetailsBtnClicked" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/confirm_contact_info" expect(result.ul).to eql "en-us" expect(result.ea).to eql "confirmContactInfo:email" expect(result.ec).to eql "enquiryFlow" expect(result.el).to eql "requestDetailsBtnClicked" end it "focus destination uni on about stay screen is correct" do result = event_gas.get_gas_by "focus:destinationUniversity" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/about_stay" expect(result.ul).to eql "en-us" expect(result.ea).to eql "aboutStay" expect(result.ec).to eql "enquiryFlow" expect(result.el).to eql "focus:destinationUniversity" end it "click submit form button on about stay screen is correct" do result = event_gas.get_gas_by "submitBtnClicked" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/about_stay" expect(result.ul).to eql "en-us" expect(result.ea).to eql "aboutStay" expect(result.ec).to eql "enquiryFlow" expect(result.el).to eql "submitBtnClicked" end it "enquiry submitted is correct" do result = event_gas.get_gas_by "cae_srp" expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/enq_submitted" expect(result.ul).to eql "en-us" expect(result.ea).to eql "success" expect(result.ec).to eql "enquiry" expect(result.el).to eql "cae_srp" end end end end
The End