Codest är främst en Ruby-butik, men ett av de många projekt som vi bygger är i JavaScript. Det är ett bibliotek för klientsidan som körs i en ganska utmanande miljö: det måste stödja i stort sett alla webbläsare som finns, inklusive mycket gamla, och dessutom interagerar det med en mängd externa skript och tjänster. Det är jättekul.
Det märkliga fallet med icke-bundlade beroenden
Med ovanstående krav kommer en hel uppsättning utmaningar som vanligtvis inte finns i en klientsida projektoch en del av dessa problem har att göra med testning. Naturligtvis har vi en oklanderlig uppsättning enhetstester och vi kör dem mot en mycket stor matris av webbläsare / operativsystemskombinationer i vår CI / CD-miljö, men det ensam utforskar inte allt som kan gå fel.
På grund av den övergripande arkitekturen i det ekosystem vi arbetar i är vi beroende av att vissa externa bibliotek laddas parallellt med våra egna - det vill säga att vi inte paketerar dem med vår kodDet kan vi inte, och det finns inget att göra åt det. Det innebär en intressant utmaning, eftersom dessa bibliotek:
- kanske inte ens finns där - om någon strular till det med att implementera vårt bibliotek,
- kan finnas där, men i felaktiga/inkompatibla versioner,
- kan ha modifierats av någon annan kod som är med på resan i en viss implementering.
Detta visar tydligt varför enhetstester inte räcker: de testar isolerat från den verkliga världen. Säg att vi mockar upp någon del av något externt biblioteks offentliga API, baserat på vad vi har upptäckt i dess dokument, och kör ett enhetstest mot det. Vad bevisar det?
Du kanske frestas att säga "det betyder att det fungerar med det externa bibliotekets API", men du skulle - tyvärr - ha fel. Det betyder bara att det interagerar korrekt med en delmängd av det externa bibliotekets offentliga API, och även då bara med den version som vi har mockat upp.
Tänk om biblioteket bokstavligen förändras framför näsan på oss? Tänk om det - där ute i naturen - får några konstiga svar som gör att det träffar en annan, odokumenterad kodväg? Kan vi ens skydda oss mot det?
Rimligt skydd
Inte 100%, nej - miljön är för komplex för det. Men vi kan vara någorlunda säkra på att allt fungerar som det ska med några generaliserade exempel på vad som kan hända med vår kod i naturen: vi kan göra integrationstester. Enhetstesterna säkerställer att vår kod körs korrekt internt, och integrationstesterna måste säkerställa att vi "pratar" korrekt med de bibliotek som vi inte kan kontrollera. Och inte med stubbar av dem heller - faktiska, levande bibliotek.
Vi kan helt enkelt använda något av de tillgängliga ramverken för integrationstest för JavaScriptVi vill bara bygga en enkel HTML-sida, lägga till några anrop till vårt bibliotek och fjärrbiblioteken på den och ge den en rejäl omgång. Vi vill dock inte översvämma någon av fjärrtjänsternas slutpunkter med anrop som genereras av våra CI/CD-miljöer. Det skulle störa viss statistik, eventuellt förstöra vissa saker och - sist men inte minst - vi skulle inte vara särskilt trevliga om vi gjorde någons produktion till en del av våra tester.
Men var det ens möjligt att integrationstesta något så komplext? Eftersom Ruby är vår första och främsta kärlek tog vi hjälp av vår expertis och började fundera på hur vi brukar göra integrationstester med fjärrtjänster i Ruby-projekt. Vi kanske använder något som vcr gem för att spela in vad som händer en gång och sedan spela upp det för våra tester när det behövs.
Ange proxy
Internt uppnår VCR detta genom att proxyförfrågningar. Det var vår aha-upplevelse. Vi behövde proxy varje begäran som inte borde träffa något på det "riktiga" internet till några stubbed svar. Då kommer den inkommande datan att överlämnas till det externa biblioteket och vår kod körs som vanligt.
När vi prototyper något som verkar komplicerat, faller vi ofta tillbaka på Ruby som en tidsbesparande metod. Vi bestämde oss för att göra en prototyp test harness för vår JavaScript i Ruby för att se hur väl proxy-idén kommer att fungera innan vi åtar oss att bygga något mer komplicerat i (eventuellt) JavaScript. Det visade sig vara förvånansvärt enkelt. Faktum är att det är så enkelt att vi kommer att bygga en i den här artikeln tillsammans. 🙂
Ljus, kamera... vänta, vi glömde rekvisitan!
Naturligtvis kommer vi inte att ha att göra med den "riktiga saken" - att förklara ens lite av vad vi bygger är långt bortom ramen för ett blogginlägg. Vi kan bygga något snabbt och enkelt för att ersätta biblioteken i fråga och sedan fokusera mer på Ruby-delen.
För det första behöver vi något som kan ersätta det externa bibliotek som vi har att göra med. Vi behöver det för att uppvisa ett par beteenden: det ska kontakta en extern tjänst, avge en händelse här och där, och framför allt - inte byggas med tanke på enkel integration 🙂
Det här är vad vi kommer att använda:
/* 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: {}
}
Du kommer att märka att det anropar ett öppet API för vissa data - i det här fallet några kryptovalutakurser, eftersom det är vad som är allt raseri nuförtiden Detta API exponerar inte en sandlåda och det är hastighetsbegränsat, vilket gör det till ett utmärkt exempel på något som faktiskt inte bör träffas i tester.
Du kanske märker att detta faktiskt är en NPM-kompatibel modul, medan jag har antytt att skriptet som vi normalt hanterar inte är tillgängligt på NPM för enkel paketering. För den här demonstrationen räcker det med att den uppvisar ett visst beteende, och jag vill hellre ha en enkel förklaring här på bekostnad av en alltför stor förenkling.
Bjuda in skådespelarna
Nu behöver vi också något som kan ersätta vårt bibliotek. Återigen håller vi kraven enkla: det måste anropa vårt "externa" bibliotek och göra något med utdata. För att hålla den "testbara" delen enkel låter vi den också göra dubbel loggning: både till konsolen, som är lite svårare att läsa i specifikationer, och till en globalt tillgänglig array.
window.remote = require('exempel på fjärrsamtal')
window.failedMiserably = sant
window.logs = []
funktion log (meddelande) {
window.logs.push(meddelande)
console.log(meddelande)
}
window.addEventListener('example:fetched', function () {
if (window.remote.error) {
log('[EXEMPEL] Fjärrhämtning misslyckades')
window.failedMiserably = true
} annars {
log('[EXEMPEL] Fjärrhämtning lyckades')
log([EXEMPEL] BTC till ETH: ${window.remote.data.BTC_ETH.last})
}
})
window.remote.fetch()
Jag håller också beteendet häpnadsväckande enkelt med avsikt. Som det är nu finns det bara två faktiskt intressanta kodvägar att specificera för, så vi kommer inte att svepas under en lavin av specifikationer när vi går vidare genom bygget.
Allt bara snäpper ihop
Vi lägger upp en enkel HTML-sida:
<code> <!DOCTYPE html>
<html>
<head>
<title>Exempel på sida</title>
<script type="text/javascript" src="./index.js"></script>
</head>
<body></body>
</html>
I den här demonstrationen kommer vi att paketera vår HTML och JavaScript tillsammans med Tomten mycket enkel webbapp buntare. Jag gillar Parcel mycket för tider som dessa, när jag kastar ihop ett snabbt exempel eller hackar på en back-of-napkin klass idé. När du gör något så enkelt att det skulle ta längre tid att konfigurera Webpack än att skriva den kod du vill ha, är det det bästa.
Det är också tillräckligt diskret för att när jag vill byta till något som är lite mer stridstestat behöver jag inte göra nästan någon backpedaling från Parcel, vilket inte är något du kan säga om Webpack. Observera dock försiktighet - Parcel är i tung utveckling och problem kan och kommer att presentera sig; Jag har haft ett problem där den transpilerade JavaScript-utgången var ogiltig på en äldre Node.js. Slutsatsen: gör det inte till en del av din produktionspipeline ännu, men ge det en snurr ändå.
Utnyttja kraften i integrationen
Nu kan vi konstruera vårt testsystem.
För själva specifikationsramverket har vi använt rspec. I utvecklingsmiljöer testar vi med riktiga, icke-huvudlösa Chrome - uppgiften att köra och kontrollera det har fallit på Watir (och dess pålitliga sidekick watir-rspec). För vår proxy har vi bjudit in Puffing Billy och rack till festen. Slutligen vill vi köra om vår JavaScript-byggnad varje gång vi kör specifikationerna, och det uppnås med kokain.
Det är en hel del rörliga delar, och därför är vår spec-hjälpare något... involverad även i det här enkla exemplet. Låt oss ta en titt på det och plocka isär det.
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::Matchare
config.include ProxySupport
config.order = :slumpmässig
BrowserSupport.configure(config)
slut
Billy.configure gör |c|
c.cache = false
c.cacherequestheaders = false
c.persistcache = false
c.recordstubrequests = true
c.logger = Logger.new(File.expandpath('../log/billy.log', FILE))
slut
Innan hela sviten kör vi vårt anpassade byggkommando genom cocaine. Den TEST_LOGGER-konstanten kan vara lite överdriven, men vi är inte särskilt bekymrade över antalet objekt här. Vi kör naturligtvis specifikationer i slumpmässig ordning, och vi måste inkludera alla godsaker från watir-rspec. Vi måste också ställa in Billy så att den inte gör någon cachelagring, men omfattande loggning till spec/log/billy.log
. Om du inte vet om en begäran faktiskt stubbas eller om den träffar en live-server (hoppsan!) är den här loggen rena guldet.
Jag är säker på att dina skarpa ögon redan har sett ProxySupport och BrowserSupport. Du kanske tror att våra anpassade godsaker sitter där inne ... och du skulle ha helt rätt! Låt oss se vad BrowserSupport gör först.
En webbläsare, kontrollerad
Låt oss först presentera TempBrowser
:
klass TempBrowser
def hämta
@webbläsare ||= Watir::Browser.new(web_driver)
slut
def kill
@browser.close if @browser
@webbläsare = nil
slut
privat
def web_driver
Selenium::WebDriver.for(:chrome, alternativ: alternativ)
slut
def alternativ
Selenium::WebDriver::Chrome::Options.new.tap do |options|
options.addargument '--auto-öppna-devtools-för-tabs'
options.addargument "--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}"
slut
slut
slut
När vi arbetar bakåt genom samtalsträdet kan vi se att vi ställer in en Selenium-webbläsaralternativuppsättning för Chrome. Ett av alternativen vi passerar in i det är instrumentellt i vår installation: det instruerar Chrome-instansen att proxy allt genom vår Puffing Billy-instans. Det andra alternativet är bara trevligt att ha - varje instans vi kör som inte är huvudlös kommer att ha inspektionsverktygen automatiskt öppna. Det sparar oss otaliga mängder Cmd + Alt + I per dag 😉
När vi har ställt in webbläsaren med dessa alternativ skickar vi den vidare till Watir och det är i stort sett allt. Den döda
metoden är lite socker som gör att vi upprepade gånger kan stoppa och starta om drivrutinen om vi behöver utan att kasta bort TempBrowser-instansen.
Nu kan vi ge våra rspec-exempel ett par superkrafter. Först och främst får vi ett smidigt webbläsare
hjälpmetod som våra specifikationer mestadels kommer att kretsa kring. Vi kan också använda oss av en praktisk metod för att starta om webbläsaren för ett visst exempel om vi gör något superkänsligt. Naturligtvis vill vi också döda webbläsaren efter att testsviten är klar, eftersom vi under inga omständigheter vill ha långvariga Chrome-instanser - för vårt RAM-minnes skull.
modul BrowserSupport
def self.browser
@webbläsare ||= TempBrowser.new
slut
def self.configure(config)
config.around(:each) do |exempel|
BrowserSupport.browser.kill if exempel.metadata[:clean]
@browser = BrowserSupport.browser.get
@browser.cookies.clear
@browser.driver.manage.timeouts.implicit_wait = 30
exempel.kör
slut
config.after(:suite) do
BrowserSupport.browser.kill
slut
slut
slut
Koppla upp proxyn
Vi har konfigurerat en webbläsare och spec-hjälpare, och vi är redo att börja proxiera förfrågningar till vår proxy. Men vänta, vi har inte satt upp den än! Vi skulle kunna göra upprepade anrop till Billy för varje exempel, men det är bättre att skaffa ett par hjälpmetoder och spara ett par tusen tangenttryckningar. Det är vad ProxySupport
gör.
Den som vi använder i vår testuppsättning är något mer komplex, men här är en allmän idé:
frozenstringliteral: true
kräver '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, fil)
Billy.proxy.stub(url).andreturn({
body: open(file).read,
kod: 200,
headers: HEADERS.dup
})
slut
def stubstatus(url, status)
Billy.proxy.stub(url).andreturn({
kropp: '',
kod: status,
headers: HEADERS.dup
})
slut
def stubpage(url, fil)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'text/html',
kod: 200
)
slut
def stubjs(url, fil)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'applikation/javascript',
kod: 200
)
slut
slut
Vi kan stubba:
- HTML-sidförfrågningar - för vår huvudsida "playground",
- JS-förfrågningar - för att betjäna vårt paketerade bibliotek,
- JSON-förfrågningar - för att stubba förfrågan till fjärr-API:et,
- och bara en "vad som helst"-förfrågan där vi bara bryr oss om att returnera ett visst HTTP-svar som inte är 200.
Detta räcker gott och väl för vårt enkla exempel. På tal om exempel - vi borde sätta upp ett par!
Testa den goda sidan
Vi måste först koppla ihop ett par "rutter" för vår proxy:
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' }
innan do
stubpage pageurl, sidväg
stubjs jsurl, jspath
slut
Det är värt att notera att från rspecs perspektiv hänvisar de relativa sökvägarna här till huvudprojektkatalogen, så vi laddar vår HTML och JS direkt från dist
katalog - som byggts av Parcel. Du kan redan nu se hur dessa stub_*
hjälpredor kommer väl till pass.
Det är också värt att notera att vi placerar vår "falska" webbplats på en .lokal
TLD. På så sätt bör inte några okontrollerade förfrågningar undkomma vår lokala miljö om något skulle gå fel. Som en allmän praxis skulle jag rekommendera att åtminstone inte använda "riktiga" domännamn i stubbar om det inte är absolut nödvändigt.
En annan anmärkning vi bör göra här handlar om att inte upprepa oss. När proxy-routingen blir mer komplex, med många fler sökvägar och webbadresser, kommer det att finnas ett verkligt värde i att extrahera den här installationen till en delad kontext och helt enkelt inkludera den efter behov.
Nu kan vi spekulera i hur vår "bra" väg ska se ut:
sammanhang "med korrekt svar" do
före do
stubjson %r{http://poloniex.com/public(.*)}, './spec/fixtures/remote.json'
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 2') }
slut
it 'loggar korrekta data' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXEMPEL] Fjärrhämtning framgångsrik', '[EXEMPEL] BTC till ETH: 0,03619999'])
)
slut
slut
Det är ganska enkelt, eller hur? Lite mer inställning här - vi stubbar JSON-svaret från fjärr-API:et med en fixtur, går till vår huvud-URL och sedan... väntar vi.
Den längsta väntetiden
Väntetiderna är ett sätt att arbeta runt en begränsning som vi har stött på med Watir - vi kan inte på ett tillförlitligt sätt vänta på t.ex. JavaScript-händelser, så vi måste fuska lite och "vänta" tills skripten har flyttat något objekt som vi kan komma åt till ett tillstånd som är av intresse för oss. Nackdelen är att om det tillståndet aldrig kommer (till exempel på grund av en bugg) måste vi vänta på att watir-servitören ska ta timeout. Detta driver upp specifikationstiden lite. Specifikationen misslyckas dock fortfarande på ett tillförlitligt sätt.
När sidan har "stabiliserats" i det tillstånd som vi är intresserade av kan vi utföra lite mer JavaScript i samband med sidan. Här ringer vi upp loggarna som skrivits till den offentliga arrayen och kontrollerar om de är vad vi förväntade oss.
Som en sidoanteckning - det är här som stubbning av fjärrförfrågan verkligen lyser. Svaret som loggas till konsolen är beroende av växelkursen som returneras av fjärr-API: et, så vi kunde inte på ett tillförlitligt sätt testa logginnehållet om de fortsatte att förändras. Det finns naturligtvis sätt att arbeta runt det, men de är inte särskilt eleganta.
Testa den dåliga grenen
Ytterligare en sak att testa: grenen "failure".
context "med misslyckat svar" do
före do
stubstatus %r{http://poloniex.com/public(.*)}, 404
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 1') }
slut
it 'fel i loggar' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXEMPEL] Fjärrhämtning misslyckades'])
)
slut
slut
Det är mycket likt ovanstående, med skillnaden att vi stubbar svaret så att det returnerar en 404 HTTP-statuskod och förväntar oss en annan logg.
Låt oss köra våra specifikationer nu.
% bunt exekvering rspec
Slumpmässigt med frö 63792
I, [2017-12-21T14:26:08.680953 #7303] INFO -- : Kommando :: npm run build
Anrop på distans
med korrekt svar
loggar korrekt data
med misslyckat svar
loggar misslyckande
Avslutades på 23,56 sekunder (filerna tog 0,86547 sekunder att ladda)
2 exempel, 0 misslyckanden
Woohoo!
Slutsats
Vi har kort diskuterat hur JavaScript kan integrationstestas med Ruby. Även om det ursprungligen ansågs vara mer av en nödlösning, är vi ganska nöjda med vår lilla prototyp nu. Vi överväger naturligtvis fortfarande en ren JavaScript-lösning, men under tiden har vi ett enkelt och praktiskt sätt att reproducera och testa några mycket komplexa situationer som vi har stött på i naturen.
Om du funderar på att bygga något liknande själv, bör det noteras att det inte är utan sina begränsningar. Till exempel om det du testar blir riktigt AJAX-tungt, kommer Puffing Billy att ta lång tid att svara. Även om du måste stubba några SSL-källor kommer det att krävas lite mer krångel - titta på watir-dokumentationen om det är ett krav du har. Vi kommer säkert att fortsätta utforska och leta efter de bästa sätten att hantera vårt unika användningsfall - och vi kommer att se till att låta dig veta vad vi fick reda på också.