摘要:持續(xù)集成單元測(cè)試是開(kāi)源的一個(gè)基于的測(cè)試執(zhí)行過(guò)程管理工具。表示測(cè)試套件,是一序列相關(guān)程序的測(cè)試表示單元測(cè)試,也就是測(cè)試的最小單位。
持續(xù)集成 單元測(cè)試(unit) karma
Karma 是Google開(kāi)源的一個(gè)基于Node.js 的 JavaScript 測(cè)試執(zhí)行過(guò)程管理工具(Test Runner)。該工具可用于測(cè)試所有主流Web瀏覽器,也可集成到 CI (Continuous integration)工具,也可和其他代碼編輯器一起使用。
我們測(cè)試用的無(wú)界面瀏覽器phantomjs。測(cè)試框架使用mocha和chai。
以下是我們項(xiàng)目中使用的主要配置信息:
/** * 測(cè)試啟動(dòng)的瀏覽器 * 可用的瀏覽器:https://npmjs.org/browse/keyword/karma-launcher */ browsers: ["PhantomJS"], /** * 測(cè)試框架 * 可用的框架:https://npmjs.org/browse/keyword/karma-adapter */ frameworks: ["mocha", "chai"], /** * 需要加載到瀏覽器的文件列表 */ files: [ "../../src/dcv/plugins/jquery/jquery-1.8.1.min.js", "../../src/dcv/plugins/common/mock.min.js", "../../src/dcv/plugins/common/bluebird.min.js", "../../src/dcv/javascripts/uinv.js", "../../src/dcv/javascripts/uinv_util.js", "../../src/dcv/javascripts/browser/uinv_browser.js", "specs/validators.js" ], /** * 排除的文件列表 */ exclude: [ ], /** * 在瀏覽器使用之前處理匹配的文件 * 可用的預(yù)處理: https://npmjs.org/browse/keyword/karma-preprocessor */ preprocessors: { //報(bào)告覆蓋 "../../src/dcv/javascripts/**/*.js": ["coverage"] }, /** * 使用測(cè)試結(jié)果報(bào)告者 * 可能的值: "dots", "progress" * 可用的報(bào)告者:https://npmjs.org/browse/keyword/karma-reporter */ reporters: ["spec", "coverage"], /** * 使用reporters為"coverage"時(shí)報(bào)告輸出的類型和那目錄 */ coverageReporter: { type: "html", dir: "coverage/" }, /** * 服務(wù)端口號(hào) */ port: 9876, /** * 啟用或禁用輸出報(bào)告或者日志中的顏色 */ colors: true, /** * 日志等級(jí) * 可能的值: * config.LOG_DISABLE //不輸出信息 * config.LOG_ERROR //只輸出錯(cuò)誤信息 * config.LOG_WARN //只輸出警告信息 * config.LOG_INFO //輸出全部信息 * config.LOG_DEBUG //輸出調(diào)試信息 */ logLevel: config.LOG_INFO, /** * 啟用或禁用自動(dòng)檢測(cè)文件變化進(jìn)行測(cè)試 */ autoWatch: true, /** * 開(kāi)啟或禁用持續(xù)集成模式 * 設(shè)置為true, Karma將打開(kāi)瀏覽器,執(zhí)行測(cè)試并最后退出 */ // singleRun: true, /** * 并發(fā)級(jí)別(啟動(dòng)的瀏覽器數(shù)) */ concurrency: Infinity
在package.json中配置如下:
"scripts": { "unit": "./node_modules/.bin/karma start test/unit/karma.conf.js --single-run" }
--single-run意思是單次執(zhí)行測(cè)試,此處會(huì)覆蓋上面的singleRun配置項(xiàng)。最終會(huì)在test/unit/coverage目錄下生成測(cè)試覆蓋率的html格式報(bào)告。
mochamocha是JavaScript的一種單元測(cè)試框架,既可以在瀏覽器環(huán)境下運(yùn)行,也可以在Node.js環(huán)境下運(yùn)行。
使用mocha,我們就只需要專注于編寫(xiě)單元測(cè)試本身,然后,讓mocha去自動(dòng)運(yùn)行所有的測(cè)試,并給出測(cè)試結(jié)果。
mocha的特點(diǎn)主要有:
既可以測(cè)試簡(jiǎn)單的JavaScript函數(shù),又可以測(cè)試異步代碼,因?yàn)楫惒绞?b>JavaScript的特性之一;
可以自動(dòng)運(yùn)行所有測(cè)試,也可以只運(yùn)行特定的測(cè)試;
可以支持before、after、beforeEach和afterEach來(lái)編寫(xiě)初始化代碼。
describe 表示測(cè)試套件,是一序列相關(guān)程序的測(cè)試;it表示單元測(cè)試(unit test),也就是測(cè)試的最小單位。例:
describe("樣例", function () { it("deep用法", function () { expect({a: 1}).to.deep.equal({a: 1}); expect({a: 1}).to.not.equal({a: 1}); expect([{a: 1}]).to.deep.include({a: 1}); // expect([{a: 1}]).to.not.include({a: 1}); expect([{a: 1}]).to.be.include({a: 1}); }); });
mocha一共四個(gè)生命鉤子
before():在該區(qū)塊的所有測(cè)試用例之前執(zhí)行
after():在該區(qū)塊的所有測(cè)試用例之后執(zhí)行
beforeEach():在每個(gè)單元測(cè)試前執(zhí)行
afterEach():在每個(gè)單元測(cè)試后執(zhí)行
利用describe.skip可以跳過(guò)測(cè)試,而不用注釋大塊代碼;異步只需要在函數(shù)中增加done回調(diào)。例:
describe.skip("異步 beforeEach 示例", function () { var foo = false; beforeEach(function (done) { setTimeout(function () { foo = true; done(); }, 50); }); it("全局變量異步修改應(yīng)該成功", function () { expect(foo).to.be.equal(true); }); it("read book async", function (done) { book.read((err, result) => { expect(err).equal(null); expect(result).to.be.a("string"); done(); }) }); });chai
chai是斷言庫(kù),可以理解為比較函數(shù),也就是斷言函數(shù)是否和預(yù)期一致,如果一致則表示測(cè)試通過(guò),如果不一致表示測(cè)試失敗。
本身mocha是不包含斷言庫(kù)的,所以必須引入第三方斷言庫(kù),目前比較受歡迎的斷言庫(kù)有 should.js、expect.js 、chai,具體的語(yǔ)法規(guī)則需要大家去查閱相關(guān)文檔。
因?yàn)?b>chai既包含should、expect和assert三種風(fēng)格,可擴(kuò)展性比較強(qiáng)。本質(zhì)是一樣的,按個(gè)人習(xí)慣選擇。詳見(jiàn)api
下面簡(jiǎn)單的介紹一下這是那種風(fēng)格
should例:
let num = 4+5 num.should.equal(9); num.should.not.equal(10); //boolean "ok".should.to.be.ok; false.should.to.not.be.ok; //type "test".should.to.be.a("string"); ({ foo: "bar" }).should.to.be.an("object");
expect例:
// equal or no equal let num = 4+5 expect(num).equal(9); expect(num).not.equal(10); //boolean expect("ok").to.be.ok; expect(false).to.not.be.ok; //type expect("test").to.be.a("string"); expect({ foo: "bar" }).to.be.an("object");
assert例:
// equal or no equal let num = 4+5 assert.equal(num,9); //type assert.typeOf("test", "string", "test is a string");端到端測(cè)試(e2e)
e2e(end to end)測(cè)試是指端到端測(cè)試,又叫功能測(cè)試,站在用戶視角,使用各種功能、各種交互,是用戶的真實(shí)使用場(chǎng)景的仿真。
在產(chǎn)品高速迭代的現(xiàn)在,有個(gè)自動(dòng)化測(cè)試,是重構(gòu)、迭代的重要保障。對(duì)web前端來(lái)說(shuō),主要的測(cè)試就是,表單、動(dòng)畫(huà)、頁(yè)面跳轉(zhuǎn)、dom渲染、Ajax等是否按照期望。
e2e測(cè)試正是保證功能的最高層測(cè)試,不關(guān)注代碼實(shí)現(xiàn)細(xì)節(jié),專注于代碼能否實(shí)現(xiàn)對(duì)應(yīng)的功能。對(duì)我們開(kāi)發(fā)人員而言,測(cè)試的主要關(guān)注點(diǎn)是映射到頁(yè)面的邏輯(一般是存儲(chǔ)的變量)是否正確。
我們使用nigthwatch來(lái)做e2e測(cè)試
nightwatchnightwatch是一個(gè)使用selenium或者webdriver或者phantomjs的nodejs編寫(xiě)的e2e自動(dòng)測(cè)試框架,可以很方便的寫(xiě)出測(cè)試用例來(lái)模仿用戶的操作來(lái)自動(dòng)驗(yàn)證功能的實(shí)現(xiàn)。
nightwatch的使用很簡(jiǎn)單,一個(gè)nightwatch.json或者nightwatch.config.js(后者優(yōu)先級(jí)高)配置文件,使用runner會(huì)自動(dòng)找同級(jí)的這兩個(gè)文件來(lái)獲取配置信息。也可以手動(dòng)使用--config來(lái)制定配置文件的相對(duì)路徑。
seleniumselenium是一個(gè)強(qiáng)大瀏覽器測(cè)試平臺(tái),支持firefox、chrome、edge等瀏覽器的模擬測(cè)試,其原理是打開(kāi)瀏覽器時(shí),把自己的JavaScript文件嵌入網(wǎng)頁(yè)中。然后selenium的網(wǎng)頁(yè)通過(guò)frame嵌入目標(biāo)網(wǎng)頁(yè)。這樣,就可以使用selenium的JavaScript對(duì)象來(lái)控制目標(biāo)網(wǎng)頁(yè)。
項(xiàng)目中nightwatch.config.js的主要配置如下:
{ "src_folders": ["test/e2e/specs"],//測(cè)試代碼所在文件夾 "output_folder": "test/e2e/reports",//測(cè)試報(bào)告所在文件夾 "globals_path": "test/e2e/global.js",//全局變量所在文件夾,可以通過(guò)browser.globals.XX來(lái)獲取 "custom_commands_path": ["node_modules/nightwatch-helpers/commands"],//自定義擴(kuò)展命令 "custom_assertions_path": ["node_modules/nightwatch-helpers/assertions"],//自定義擴(kuò)展斷言 "selenium": { "start_process": true, "server_path": seleniumServer.path,//selenium的服務(wù)所在地址,一般是個(gè)jar包 "host": "127.0.0.1", "port": 4444, "cli_args": { "webdriver.chrome.driver": chromedriver.path,//谷歌瀏覽器的drvier地址,在windows下是個(gè)exe文件 "webdriver.firefox.profile": "", "webdriver.ie.driver": "", "webdriver.phantomjs.driver": phantomjsDriver.path } }, "test_settings": { "phantomjs": { "desiredCapabilities": { "browserName": "phantomjs", "marionette": true, "acceptSslCerts": true, "phantomjs.binary.path": phantomjsDriver.path, "phantomjs.cli.args": ["--ignore-ssl-errors=false"] } }, "chrome": { "desiredCapabilities": { "browserName": "chrome", "javascriptEnabled": true, "acceptSslCerts": true, "chromeOptions": { "args": [ // "start-fullscreen" // "--headless", //開(kāi)啟無(wú)界面 // "--disable-gpu" ] } } }, "firefox": { "desiredCapabilities": { "browserName": "firefox", "javascriptEnabled": true, "acceptSslCerts": true } }, "ie": { "desiredCapabilities": { "browserName": "internet explorer", "javascriptEnabled": true, "acceptSslCerts": true } } } }
在package.json中配置如下:
"scripts": { "e2e_ci": "node test/e2e/runner.js --env phantomjs", "e2e_parallel": "node test/e2e/runner.js --env phantomjs,chrome" }
以上2個(gè)命令都是執(zhí)行runner.js文件,前者配置了個(gè)環(huán)境變量phantomjs,這樣就會(huì)在上面查找test_settings中的phantomjs;后者并發(fā)執(zhí)行,同時(shí)用phantomjs和chrome瀏覽器進(jìn)行測(cè)試。
測(cè)試代碼凡是在上述src_folders文件夾下的js文件,都會(huì)被認(rèn)為是測(cè)試代碼,會(huì)執(zhí)行測(cè)試。要跳過(guò)測(cè)試,有幾種方式:
@disabled,這樣整個(gè)文件會(huì)跳過(guò)測(cè)試
@tags標(biāo)簽,多個(gè)文件可以標(biāo)記一樣的標(biāo)簽??梢悦钚兄刑砑?b>--tag manager,這樣,只會(huì)測(cè)試標(biāo)簽為manager的js文件,其它都會(huì)略過(guò)
如果只是想跳過(guò)當(dāng)前文件的某個(gè)測(cè)試方法,可以將function轉(zhuǎn)換為字符串,比如
module.exports = { "step1": function (browser) { }, "step2": "" + function (browser) { } }
以下是項(xiàng)目中一個(gè)樣例,幾乎涵蓋了各種操作。具體可參看http://nightwatchjs.org/api
var path = require("path"); module.exports = { //"@disabled": true, //不執(zhí)行這個(gè)測(cè)試模塊 "@tags": ["manager"],//標(biāo)簽 "test manager": function (browser) { const batchFile = browser.globals.batchFile; const url = browser.globals.managerURL; browser .url(url) .getCookie("token", function (result) { if (result) { // browser.deleteCookie("token"); } else { this .waitForElementVisible("#loginCode", 50) .setValue("#loginCode", browser.globals.userName) .setValue("#loginPwd", browser.globals.password) .element("css selector", "#mntCode", function (res) { //判斷是否有多租戶 if (res.status != -1) { browser .click("#mntCode", function () { browser .assert.cssProperty("#mntList", "display", "block") //展示多租戶列表 .assert.elementPresent("#mntList li[value=uinnova]"); }) .pause(500) .moveToElement("#mntList li[value=uinnova]", 0, 0, function () { //將鼠標(biāo)光標(biāo)移動(dòng)到優(yōu)锘 browser.click("#mntList li[value=uinnova]", function () { browser.assert.containsText("#mntCode", "優(yōu)锘科技"); }); }); } }) .click("#fm-login-submit") .pause(50) .url(function (res) { if (res.value !== url) { //這個(gè)命令可以用來(lái)截圖 browser.saveScreenshot(browser.globals.imagePath + "login.png"); } }) .assert.urlContains(url, "判斷有沒(méi)有跳轉(zhuǎn)成功,否則即是登陸失敗"); .execute(function (param) { //此處可以執(zhí)行頁(yè)面中的代碼,且得到后面?zhèn)鬟f的參數(shù) try { return uinv.data3("token"); } catch (e) { } }, ["param1"], function (res) { //此處可以得到上面方法返回值 }); } }) .maximizeWindow() //窗口最大化 .waitForElementVisible("#app", 1000) .pause(1000) .elements("css selector", ".data .clear li", function (res) { var nums = res.value.length - 1; //獲取到manage.html頁(yè)面中場(chǎng)景的個(gè)數(shù) browser.expect.element(".data_num").text.to.equal("(" + nums + ")"); // 用來(lái)統(tǒng)計(jì)場(chǎng)景個(gè)數(shù)的sapn標(biāo)簽中的值是否等于實(shí)際的場(chǎng)景個(gè)數(shù) browser.pause(500); }) .click(".clear .last .add_data") .waitForElementPresent("#dcControlFrame") .frame("dcControlFrame", function () { //定位到頁(yè)面中的iframe,需要填寫(xiě)iframe的id(不需要加#) browser .waitForElementPresent("#dataCenterId") .saveScreenshot(browser.globals.imagePath + "dcControlFrame.png") .setValue("#dataCenterId", browser.globals.sceneId) .setValue("#dataCenterName", browser.globals.sceneName) .setValue("#dataCenterText", "歡迎光臨") .setValue("#up_picture[type="file"]", path.resolve(batchFile + "/color.png")) //上傳圖片 .click(".group-btn .save", function () { browser .pause(1000) .click(".layui-layer-btn0"); }) .waitForElementVisible("#dataCenterMenu3", 1000) .pause(1500) //上傳場(chǎng)景 .click("#dataCenterMenu3", function () { browser .setValue("#img-3d-max-model input[type="file"]", path.resolve(batchFile + "/20121115uinnovaDEMO.zip")) //上傳場(chǎng)景文件 .waitForElementVisible(".layui-layer-btn0", 20000, function () { browser .click(".layui-layer-btn0"); }) .setValue("#img-3d-max-layout input[type="file"]", path.resolve(batchFile + "/DEMO20140424-2016-01-14-17-48-17.js")) //上傳布局文件 .waitForElementVisible(".layui-layer-btn0", 5000, function () { browser .click(".layui-layer-btn0"); }); }) .pause(500) .saveScreenshot(browser.globals.imagePath + "frameParentBefore.png"); }) // .frameParent() //回到iframe的父級(jí)頁(yè)面;//TODO 無(wú)界面下,frame退出有問(wèn)題,所以暫時(shí)改用refresh重新刷新頁(yè)面 .refresh() .end(); } };
以下是XX同學(xué)的使用總結(jié)
有些情況下延時(shí)(pause)是必須的,比如在表單操作中需要上傳圖片,需要等文件上傳成功后再點(diǎn)擊保存按鈕
接著第一條說(shuō),用pause就必須傳入一個(gè)固定時(shí)毫秒值,數(shù)值太大浪費(fèi)時(shí)間,數(shù)值太小可能未執(zhí)行完畢,需要反復(fù)測(cè)試。如果可以的話,可以使用 waitForElementVisible 類的方法,時(shí)間設(shè)置的長(zhǎng)些也無(wú)妨。
command方法的回調(diào)函數(shù)中的返回值會(huì)是一個(gè)對(duì)象,先把這個(gè)對(duì)象打印出來(lái)看一下格式,再使用這個(gè)對(duì)象
所有的assert和command最后都有一個(gè)可選參數(shù),自定義測(cè)試通過(guò)時(shí)命令行提示信息
附錄 phantomjsPhantomJS是一個(gè)基于webkit的JavaScript API。它使用QtWebKit作為它核心瀏覽器的功能,使用webkit來(lái)編譯解釋執(zhí)行JavaScript代碼。任何你可以在基于webkit瀏覽器做的事情,它都能做到。它不僅是個(gè)隱形的瀏覽器,提供了諸如CSS選擇器、支持Web標(biāo)準(zhǔn)、DOM操作、JSON、HTML5、Canvas、SVG等,同時(shí)也提供了處理文件I/O的操作,從而使你可以向操作系統(tǒng)讀寫(xiě)文件等。PhantomJS的用處可謂非常廣泛,諸如網(wǎng)絡(luò)監(jiān)測(cè)、網(wǎng)頁(yè)截屏、無(wú)需瀏覽器的 Web 測(cè)試、頁(yè)面訪問(wèn)自動(dòng)化等。
因?yàn)?b>phantomjs本身并不是一個(gè)nodejs庫(kù),所以我們使用的其實(shí)是phantomjs-prebuilt這個(gè)包,它會(huì)根據(jù)當(dāng)前操作系統(tǒng)判斷從phantomjs官網(wǎng)下載驅(qū)動(dòng)包。
遺憾的是,PhantomJS 的核心開(kāi)發(fā)者之一 Vitaly Slobodin 近日宣布,已辭任 maintainer ,不再維護(hù)項(xiàng)目。
Vitaly 發(fā)文表示,Chrome 59 將支持 headless 模式,用戶最終會(huì)轉(zhuǎn)向去使用它。Chrome 比PhantomJS 更快,更穩(wěn)定,也不會(huì)像 PhantomJS 這樣瘋狂吃內(nèi)存:
“我看不到 PhantomJS 的未來(lái),作為一個(gè)多帶帶的開(kāi)發(fā)者去開(kāi)發(fā) PhantomJS 2 和 2.5 ,簡(jiǎn)直就像是一個(gè)血腥的地獄。即便是最近發(fā)布的 2.5 Beta 版本擁有全新、亮眼的 QtWebKit ,但我依然無(wú)法做到真正的支持 3 個(gè)平臺(tái)。我們沒(méi)有得到其他力量的支持!”
隨著 Vitaly 的退出,項(xiàng)目?jī)H剩下兩位核心開(kāi)發(fā)者進(jìn)行維護(hù)。
上面也有說(shuō)到,項(xiàng)目并未得到資源支持,如此大型的項(xiàng)目,就算兩人正職維護(hù),也很艱難。
缺陷雖然Phantom.js 是fully functional headless browser,但是它和真正的瀏覽器還是有很大的差別,并不能完全模擬真實(shí)的用戶操作。很多時(shí)候,我們?cè)?b>Phantom.js發(fā)現(xiàn)一些問(wèn)題,但是調(diào)試了半天發(fā)現(xiàn)是Phantom.js自己的問(wèn)題。
將近2k的issue,仍然需要人去修復(fù)。
Javascript天生單線程的弱點(diǎn),需要用異步方式來(lái)模擬多線程,隨之而來(lái)的callback地獄,對(duì)于新手而言非常痛苦,不過(guò)隨著es6的廣泛應(yīng)用,我們可以用promise來(lái)解決多重嵌套回調(diào)函數(shù)的問(wèn)題。
雖然webdriver支持htmlunit與phantomjs,但由于沒(méi)有任何界面,當(dāng)我們需要進(jìn)行調(diào)試或復(fù)現(xiàn)問(wèn)題時(shí),就非常麻煩。
PuppeteerPuppeteer是谷歌官方出品的一個(gè)通過(guò)DevTools協(xié)議控制headless Chrome的Node庫(kù)??梢酝ㄟ^(guò)Puppeteer的提供的api直接控制Chrome模擬大部分用戶操作來(lái)進(jìn)行UI Test或者作為爬蟲(chóng)訪問(wèn)頁(yè)面來(lái)收集數(shù)據(jù)。類似于webdriver的高級(jí)別的api,去幫助我們通過(guò)DevTools協(xié)議控制無(wú)界面Chrome。
在puppteteer之前,我們要控制chrome headless需要使用chrome-remote-interface來(lái)實(shí)現(xiàn),但是它比 Puppeteer API 更接近低層次實(shí)現(xiàn),無(wú)論是閱讀還是編寫(xiě)都要比puppteteer更復(fù)雜。也沒(méi)有具體的dom操作,尤其是我們要模擬一下click事件,input事件等,就顯得力不從心了。
我們用同樣2段代碼來(lái)對(duì)比一下2個(gè)庫(kù)的區(qū)別。
首先來(lái)看看 chrome-remote-interface
const chromeLauncher = require("chrome-launcher"); const CDP = require("chrome-remote-interface"); const fs = require("fs"); function launchChrome(headless=true) { return chromeLauncher.launch({ // port: 9222, // Uncomment to force a specific port of your choice. chromeFlags: [ "--window-size=412,732", "--disable-gpu", headless ? "--headless" : "" ] }); } (async function() { const chrome = await launchChrome(); const protocol = await CDP({port: chrome.port}); const {Page, Runtime} = protocol; await Promise.all([Page.enable(), Runtime.enable()]); Page.navigate({url: "https://www.github.com/"}); await Page.loadEventFired( console.log("start") ); const {data} = await Page.captureScreenshot(); fs.writeFileSync("example.png", Buffer.from(data, "base64")); // Wait for window.onload before doing stuff. protocol.close(); chrome.kill(); // Kill Chrome.
再來(lái)看看 puppeteer
const puppeteer = require("puppeteer"); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://www.github.com"); await page.screenshot({path: "example.png"}); await browser.close(); })();
就是這么簡(jiǎn)短明了,更接近自然語(yǔ)言。沒(méi)有callback,幾行代碼就能搞定我們所需的一切。
再來(lái)段打印阮一峰大神的《ECMAScript 6 入門(mén)》的pdf文檔的例子:
const puppeteer = require("puppeteer"); const getRootDir = require("root-directory"); (async () => { const rootDir = await getRootDir(); let pdfDir = rootDir + "/public/pdf/es6-pdf/"; const browser = await puppeteer.launch({ headless: false, devtools: true //開(kāi)發(fā),在headless為true時(shí)很有用 }); let page = await browser.newPage(); await page.goto("http://es6.ruanyifeng.com/#README"); await page.waitFor(2000); const aTags = await page.evaluate(() => { let as = [...document.querySelectorAll("ol li a")]; return as.map((a) => { return { href: a.href.trim(), name: a.text }; }); }); if (!aTags) { browser.close(); return; } await page.pdf({path: pdfDir + `${aTags[0].name}.pdf`}); page.close(); // 這里也可以使用promise all,但cpu可能吃緊,謹(jǐn)慎操作 for (var i = 1; i < aTags.length; i++) { page = await browser.newPage(); var a = aTags[i]; await page.goto(a.href); await page.waitFor(2000); await page.pdf({path: pdfDir + `${a.name}.pdf`}); console.log(a.name); page.close(); } browser.close(); })();
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/104874.html
摘要:來(lái)這里看看的工程師如何進(jìn)行持續(xù)集成與持續(xù)部署。主要介紹了豆瓣移動(dòng)持續(xù)集成和測(cè)試相關(guān)實(shí)踐,用工具化自動(dòng)化社會(huì)化測(cè)試來(lái)解決遇到的問(wèn)題,將打包發(fā)布環(huán)節(jié)自動(dòng)化。這期的持續(xù)集成實(shí)踐分享就到這里。 我們??吹皆S多團(tuán)隊(duì)和開(kāi)發(fā)者分享他們的持續(xù)集成實(shí)踐經(jīng)驗(yàn),本期 fir.im Weekly 收集了 iOS,Android,PHP ,NodeJS 等項(xiàng)目搭建持續(xù)集成的實(shí)踐,以及一些國(guó)內(nèi)外公司的內(nèi)部持續(xù)集成...
閱讀 2896·2023-04-25 18:58
閱讀 1011·2021-11-25 09:43
閱讀 1241·2021-10-25 09:46
閱讀 3527·2021-09-09 11:40
閱讀 1744·2021-08-05 09:59
閱讀 893·2019-08-29 15:07
閱讀 981·2019-08-29 12:48
閱讀 730·2019-08-29 11:19