Ačkoli je Codest především Ruby shop, jeden z mnoha projektů, které stavíme, je v JavaScript. Jedná se o knihovnu na straně klienta, která běží v poměrně náročném prostředí: musí podporovat téměř všechny existující prohlížeče, včetně těch velmi starých, a navíc spolupracuje s množstvím externích skriptů a služeb. Je to spousta zábavy.
Zvláštní případ nesvazkových závislostí
S výše uvedenými požadavky se pojí celá řada problémů, které se u klientských systémů obvykle nevyskytují. projekta jedna z těchto otázek se týká testování. Samozřejmě máme bezvadnou sadu jednotkových testů a v našem prostředí CI/CD je spouštíme proti velmi rozsáhlé matici kombinací prohlížečů a operačních systémů, ale to samo o sobě nezkoumá vše, co se může pokazit.
Vzhledem k zastřešující architektuře ekosystému, ve kterém pracujeme, jsme závislí na tom, že některé externí knihovny jsou načteny společně s našimi - to znamená, že je nepřibalujeme k našim knihovnám. kód; nemůžeme a nedá se s tím nic dělat. To představuje zajímavou výzvu, protože tyto knihovny:
- možná ani nebude - pokud někdo pokazí implementaci naší knihovny,
- mohou být k dispozici, ale v nesprávných/nekompatibilních verzích,
- mohl být upraven jiným kódem, který je v konkrétní implementaci součástí systému.
To jasně ukazuje, proč nestačí jednotkové testy: testují izolovaně od reálného světa. Řekněme, že na základě toho, co jsme zjistili v dokumentaci, vytvoříme maketu nějaké části veřejného API externí knihovny a spustíme proti ní jednotkový test. Co to dokazuje?
Možná budete v pokušení říct "to znamená, že to funguje s rozhraním API externí knihovny", ale to se bohužel mýlíte. Znamená to pouze, že správně spolupracuje s podmnožinou veřejného API externí knihovny, a to ještě pouze s verzí, kterou jsme si vymodelovali.
Co když se knihovna doslova změní zpod nás? Co když - ve volné přírodě - dostane nějaké podivné reakce, které ho donutí zasáhnout jinou, nedokumentovanou cestu kódu? Můžeme se proti tomu vůbec chránit?
Přiměřená ochrana
Ne 100%, ne - na to je prostředí příliš složité. Ale můžeme si být dostatečně jisti, že vše funguje tak, jak má, pomocí několika zobecněných příkladů toho, co by se s naším kódem mohlo stát v přírodě: můžeme provést integrační testování. Jednotkové testy zajišťují, že náš kód běží správně interně, a integrační testy musí zajistit, že si správně "povídáme" s knihovnami, které nemůžeme ovládat. A to ani s jejich odnoží - se skutečnými, živými knihovnami.
Mohli bychom použít některý z dostupných integračních testovacích rámců pro JavaScript, vytvořte jednoduchou stránku HTML, předhoďte na ni několik volání naší knihovny a vzdálených knihoven a pořádně ji procvičte. Nechceme však zahltit žádný z koncových bodů vzdálených služeb voláními generovanými našimi prostředími CI/CD. To by nám rozhodilo některé statistiky, možná by to něco rozbilo a - v neposlední řadě - nebylo by moc hezké, kdybychom z něčí produkce udělali součást našich testů.
Bylo ale vůbec možné něco tak složitého testovat? Protože Ruby je naší první a hlavní láskou, vrátili jsme se k našim odborným znalostem a začali přemýšlet o tom, jak obvykle provádíme integrační testování se vzdálenými službami v projektech Ruby. Mohli bychom použít něco jako např. vcr klenot, aby jednou zaznamenal, co se děje, a pak to v případě potřeby přehrával našim testům.
Zadejte proxy
Vnitřně toho vcr dosahuje prostřednictvím proxy požadavků. To byl náš a-ha! moment. Potřebovali jsme proxy server pro každý požadavek, který by neměl zasáhnout nic na "skutečném" internetu, na nějaké stubbed odpovědi. Pak se tato příchozí data předají externí knihovně a náš kód běží jako obvykle.
Když vytváříme prototyp něčeho, co se zdá být složité, často se vracíme k jazyku Ruby jako k metodě, která šetří čas. Rozhodli jsme se vytvořit prototyp testovací svazek pro náš JavaScript v jazyce Ruby, abychom zjistili, jak dobře bude myšlenka proxy fungovat, než se pustíme do vytváření něčeho složitějšího v (možná) JavaScript. Ukázalo se, že je to překvapivě jednoduché. Vlastně je to tak jednoduché, že si ho v tomto článku společně postavíme 🙂.
Světla, kamera... počkat, zapomněli jsme na rekvizity!
Samozřejmě se nebudeme zabývat "skutečnými věcmi" - vysvětlení byť jen části toho, co stavíme, je daleko za hranicemi rozsahu příspěvku na blogu. Můžeme vytvořit něco rychlého a jednoduchého, co bude stát v pozadí daných knihoven, a pak se více soustředit na část týkající se Ruby.
Nejdříve potřebujeme něco, co bude zastupovat externí knihovnu, se kterou pracujeme. Potřebujeme, aby vykazovala několik chování: měla by kontaktovat externí službu, tu a tam vyslat nějakou událost a především - neměla by být vytvořena s ohledem na snadnou integraci 🙂.
Použijeme tyto informace:
/* global XMLHttpRequest, Event */
const URL = 'https://poloniex.com/public/?command=returnTicker'
const METHOD = 'GET'
module.exports = {
fetch: function () {
var req = new XMLHttpRequest()
req.responseType = 'json'
req.open(METHOD, URL, true)
var doneEvent = new Event('example:fetched')
req.onreadystatechange = function (aEvt) {
if (req.readyState === 4) {
if (req.status === 200) {
this.data = req.response
} else {
this.error = true
}
window.dispatchEvent(doneEvent)
}
}.bind(this)
req.send(null)
},
error: false,
data: {}
}
Všimněte si, že volá otevřené rozhraní API pro některá data - v tomto případě pro kurzy kryptoměn, protože to je v současné době v módě Toto rozhraní API nevystavuje sandbox a je omezeno rychlostí, což z něj dělá ukázkový příklad něčeho, co by se v testech nemělo používat.
Možná jste si všimli, že se ve skutečnosti jedná o modul kompatibilní s NPM, zatímco jsem naznačil, že skript, kterým se obvykle zabýváme, není k dispozici v NPM pro snadné sdružování. Pro tuto ukázku stačí, že vykazuje určité chování, a raději bych zde měl snadné vysvětlení za cenu přílišného zjednodušení.
Pozvání herců
Nyní potřebujeme také něco, co by zastoupilo naši knihovnu. Požadavky budou opět jednoduché: musí to volat naši "externí" knihovnu a něco dělat s výstupem. V zájmu zachování jednoduchosti "testovatelné" části ji také necháme provádět dvojí logování: jednak do konzole, což je ve specifikacích trochu hůře čitelné, jednak do globálně dostupného pole.
window.remote = require('remote-calling-example')
window.failedMiserably = true
window.logs = []
function log (zpráva) {
window.logs.push(zpráva)
console.log(zpráva)
}
window.addEventListener('example:fetched', function () {
if (window.remote.error) {
log('[EXAMPLE] Remote fetch failed')
window.failedMiserably = true
} else {
log('[EXAMPLE] Remote fetch successful')
log([EXAMPLE] BTC to ETH: ${okno.remote.data.BTC_ETH.last})
}
})
window.remote.fetch()
Záměrně také udržuji chování ohromně jednoduché. Už takhle jsou tu jen dvě skutečně zajímavé cesty kódu, které je třeba specifikovat, takže nás při postupu sestavování nezavalí lavina specifikací.
Všechno to do sebe zapadá
Vytvoříme jednoduchou stránku HTML:
<code> <!DOCTYPE html>
<html>
<head>
<title>Příklad stránky</title>
<script type="text/javascript" src="./index.js"></script>
</head>
<body></body>
</html>
Pro účely této ukázky spojíme naše HTML a JavaScript dohromady s. Parcela, velmi jednoduchý webová aplikace bundler. Parcel se mi hodně líbí v takových chvílích, kdy dávám dohromady rychlý příklad nebo se snažím nahodit nápad na třídu. Když děláte něco tak jednoduchého, že by konfigurace Webpacku trvala déle než psaní požadovaného kódu, je to nejlepší.
Je také natolik nenápadný, že když chci přejít na něco, co je trochu více vyzkoušené, nemusím z Parcelu téměř vůbec couvat, což se o Webpacku říct nedá. Pozor však - Parcel je ve fázi intenzivního vývoje a problémy se mohou objevit a objeví; měl jsem problém, kdy transpilovaný výstup JavaScript byl neplatný na starším počítači. Node.js. Sečteno a podtrženo: zatím ji nezařazujte do svého výrobního procesu, ale přesto ji vyzkoušejte.
Využití síly integrace
Nyní můžeme sestavit náš testovací svazek.
Pro samotný rámec specifikace jsme použili rspec. Ve vývojových prostředích testujeme pomocí skutečného prohlížeče Chrome bez hlavy - úkol spustit a kontrolovat tento prohlížeč připadl na. watir (a jeho věrný pomocník watir-rspec). Pro našeho zástupce jsme pozvali Puffing Billy a stojan na večírek. Nakonec chceme při každém spuštění specifikací znovu spustit sestavení JavaScript, čehož dosáhneme pomocí příkazu kokain.
To je celá řada pohyblivých částí, takže náš pomocník pro specifikace je i v tomto jednoduchém příkladu poněkud... zapojen. Podívejme se na něj a rozeberme ho.
Dir['./spec/support/*/.rb'].each { |f| require f }
TEST_LOGGER = Logger.new(STDOUT)
RSpec.configure do |config|
config.before(:suite) { Cocaine::CommandLine.new('npm', 'run build', logger: TEST_LOGGER).run }
config.include Watir::RSpec::Helper
config.include Watir::RSpec::Matchers
config.include ProxySupport
config.order = :random
BrowserSupport.configure(config)
konec
Billy.configure do |c|
c.cache = false
c.cacherequestheaders = false
c.persistcache = false
c.recordstubrequests = true
c.logger = Logger.new(File.expandpath('../log/billy.log', FILE))
end
Před celou sadou spustíme náš vlastní příkaz pro sestavení pomocí programu Cocaine. Ta konstanta TEST_LOGGER je možná trochu přehnaná, ale nás zde počet objektů příliš nezajímá. Spouštíme samozřejmě specifikace v náhodném pořadí a potřebujeme zahrnout všechny dobroty z watir-rspec. Musíme také nastavit Billy tak, aby neprovádělo žádné ukládání do mezipaměti, ale rozsáhlé logování do spec/log/billy.log. Pokud nevíte, zda se požadavek skutečně stubbuje, nebo zda se dostává na živý server (jéje!), je tento protokol naprostým zlatem.
Jsem si jistý, že vaše bystré oči si již všimly ProxySupport a BrowserSupport. Možná si říkáte, že tam sedí naše vlastní dobroty... a máte naprostou pravdu! Podívejme se nejprve, co dělá BrowserSupport.
Prohlížeč, řízený
Nejprve si představíme TempBrowser:
třída TempBrowser
def get
@browser ||= Watir::Browser.new(web_driver)
end
def kill
@browser.close if @browser
@browser = nil
end
private
def web_driver
Selenium::WebDriver.for(:chrome, options: options)
end
def options
Selenium::WebDriver::Chrome::Options.new.tap do |options|
options.addargument '--auto-open-devtools-for-tabs'
options.addargument "--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}"
konec
end
end
Když projdeme stromem volání zpět, zjistíme, že nastavujeme sadu možností prohlížeče Selenium pro Chrome. Jedna z voleb, kterou do ní předáváme, je pro naše nastavení klíčová: dává instanci Chrome pokyn, aby vše proxyovala přes naši instanci Puffing Billy. Druhá možnost je jen příjemná - každá instance, kterou spustíme a která není bezhlavý se automaticky otevřou kontrolní nástroje. To nám ušetří nespočetné množství Cmd+Alt+I za den 😉.
Poté, co prohlížeč nastavíme pomocí těchto možností, předáme jej společnosti Watir a to je v podstatě vše. Stránka zabít metoda je tak trochu cukr, který nám umožňuje opakovaně zastavit a znovu spustit ovladač, pokud potřebujeme, aniž bychom museli zahodit instanci TempBrowseru.
Nyní můžeme dát našim příkladům rspec několik superschopností. Především získáme šikovnou funkci prohlížeč pomocné metody, kolem které se budou naše specifikace převážně točit. Můžeme také využít šikovnou metodu pro restartování prohlížeče pro konkrétní příklad, pokud děláme něco supercitlivého. Samozřejmě také chceme prohlížeč po dokončení testovací sady zabít, protože v žádném případě nechceme, aby v něm zůstávaly instance Chrome - kvůli naší paměti RAM.
modul BrowserSupport
def self.browser
@browser ||= TempBrowser.new
konec
def self.configure(config)
config.around(:each) do |example|
BrowserSupport.browser.kill if example.metadata[:clean]
@browser = BrowserSupport.browser.get
@browser.cookies.clear
@browser.driver.manage.timeouts.implicit_wait = 30
example.run
end
config.after(:suite) do
BrowserSupport.browser.kill
end
end
end
Zapojení proxy serveru
Máme nastavený prohlížeč a pomocníky specifikací a jsme připraveni začít odesílat požadavky na náš proxy server. Ale počkejte, ještě jsme ho nenastavili! Mohli bychom nabouchat opakované volání Billyho pro každý příklad, ale je lepší si pořídit pár pomocných metod a ušetřit si pár tisíc stisků kláves. To je to, co ProxySupport dělá.
Ten, který používáme v našem testovacím nastavení, je o něco složitější, ale zde je obecná představa:
frozenstringliteral: true
require 'json'
modul ProxySupport
HEADERS = {
'Access-Control-Allow-Methods' => 'GET',
'Access-Control-Allow-Headers' => 'X-Requested-With, X-Prototype-Version, Content-Type',
'Access-Control-Allow-Origin' => '*'
}.freeze
def stubjson(url, file)
Billy.proxy.stub(url).andreturn({
body: open(file).read,
code: 200,
headers: HEADERS.dup
})
konec
def stubstatus(url, status)
Billy.proxy.stub(url).andreturn({
body: '',
code: status,
headers: HEADERS.dup
})
konec
def stubpage(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'text/html',
code: 200
)
konec
def stubjs(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'application/javascript',
code: 200
)
konec
konec
Můžeme si stoupnout:
- Požadavky na stránku HTML - pro naši hlavní stránku "hřiště",
- Požadavky JS - k obsluze naší svazkové knihovny,
- Požadavky JSON - pro zadání požadavku na vzdálené rozhraní API,
- a požadavek "cokoli", u kterého nás zajímá pouze vrácení konkrétní odpovědi HTTP, která není 200.
Pro náš jednoduchý příklad to bude stačit. Když už mluvíme o příkladech - měli bychom jich pár vytvořit!
Testování dobré stránky
Nejprve musíme pro náš proxy server vytvořit několik "tras":
let(:pageurl) { 'http://myfancypage.local/index.html' }
let(:jsurl) { 'http://myfancypage.local/dist/remote-caller-example.js' }
let(:pagepath) { './dist/index.html' }
let(:jspath) { './dist/remote-caller-example.js' }
before do
stubpage pageurl, pagepath
stubjs jsurl, jspath
end
Stojí za zmínku, že z pohledu rspec se zde relativní cesty vztahují k hlavnímu adresáři projektu, takže načítáme naše HTML a JS přímo z adresáře. dist adresář - jak jej sestavil Parcel. Již nyní můžete vidět, jak tyto stub_* se hodí pomocníci.
Za zmínku stojí také to, že naše "falešné" webové stránky umisťujeme na. .local TLD. Tímto způsobem by nemělo dojít k úniku požadavků z našeho místního prostředí, pokud by se něco pokazilo. Jako obecnou praxi bych doporučoval alespoň nepoužívat "skutečná" doménová jména ve stubs, pokud to není nezbytně nutné.
Další poznámka, kterou bychom zde měli uvést, se týká toho, abychom se neopakovali. Jak se směrování proxy serverů stává složitějším, s mnohem více cestami a adresami URL, bude mít skutečnou hodnotu extrahovat toto nastavení do sdíleného kontextu a jednoduše ho zahrnout podle potřeby.
Nyní můžeme specifikovat, jak by měla vypadat naše "dobrá" cesta:
context 'se správnou odpovědí' do
before do
stubjson %r{http://poloniex.com/public(.*)}, './spec/fixtures/remote.json'
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 2') }
end
it 'logs proper data' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXAMPLE] Remote fetch successful', '[EXAMPLE] BTC to ETH: 0.03619999'])
)
end
end
To je docela jednoduché, že? Ještě trochu nastavení - odpověď JSON ze vzdáleného rozhraní API přerušíme pomocí fixu, přejdeme na naši hlavní adresu URL a pak... čekáme.
Nejdelší čekání
Čekání je způsob, jak obejít omezení, na které jsme narazili u systému Watir - nemůžeme spolehlivě čekat např. na události JavaScript, takže musíme trochu podvádět a "čekat", až skripty přesunou nějaký objekt, ke kterému máme přístup, do stavu, který nás zajímá. Nevýhodou je, že pokud tento stav nikdy nenastane (např. kvůli chybě), musíme čekat, až watir waiter vyprší. To trochu prodlužuje čas specifikace. Specifikace však stále spolehlivě selhává.
Poté, co se stránka "ustálí" na stavu, který nás zajímá, můžeme v kontextu stránky provést další JavaScript. Zde vyvoláme protokoly zapsané do veřejného pole a zkontrolujeme, zda jsou takové, jaké jsme očekávali.
Poznámka na okraj - v tomto případě se vzdálený požadavek opravdu vyplatí. Odpověď, která se zaznamená do konzoly, je závislá na kurzu vráceném vzdáleným rozhraním API, takže bychom nemohli spolehlivě otestovat obsah protokolu, kdyby se neustále měnil. Existují samozřejmě způsoby, jak to obejít, ale nejsou příliš elegantní.
Testování špatné větve
Ještě jedna věc k otestování: větev "selhání".
context 's neúspěšnou odpovědí' do
before do
stubstatus %r{http://poloniex.com/public(.*)}, 404
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 1') }
end
it 'logs failure' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXAMPLE] Remote fetch failed'])
)
end
end
Je to velmi podobné výše uvedenému, s tím rozdílem, že odpověď vrátí stavový kód HTTP 404 a očekáváme jiný protokol.
Spusťme nyní naše specifikace.
% balíček exec rspec
Randomizováno s osivem 63792
I, [2017-12-21T14:26:08.680953 #7303] INFO -- : Příkaz :: npm run build
Vzdálené volání
se správnou odpovědí
zaznamenává správná data
s chybnou odpovědí
zaznamená selhání
Dokončeno za 23,56 sekundy (načtení souborů trvalo 0,86547 sekundy)
2 příklady, 0 selhání
Woohoo!
Závěr
Stručně jsme probrali, jak lze JavaScript integrovat s jazykem Ruby. I když jsme to původně považovali spíše za provizorium, nyní jsme s naším malým prototypem docela spokojeni. Stále samozřejmě uvažujeme o čistém řešení JavaScript, ale zatím máme jednoduchý a praktický způsob, jak reprodukovat a testovat některé velmi složité situace, se kterými jsme se setkali ve volné přírodě.
Pokud uvažujete o tom, že si něco podobného postavíte sami, je třeba poznamenat, že to není bez omezení. Například pokud bude testovaný obsah opravdu náročný na AJAX, bude Puffing Billy dlouho reagovat. Také pokud budete muset stubovat některé zdroje SSL, bude zapotřebí trochu více práce - pokud je to váš požadavek, podívejte se do dokumentace watiru. Určitě budeme pokračovat ve zkoumání a hledání nejlepších způsobů, jak se vypořádat s naším jedinečným případem použití - a určitě vás také budeme informovat o tom, na co jsme přišli.